├── .gitattributes ├── assets └── logo.png ├── rollup.config.js ├── defaults └── assets │ ├── remove_dependencies.sh │ ├── restart_network_services.sh │ ├── extract_network_config.sh │ ├── stop_hotspot.sh │ ├── start_dhcp_server.sh │ ├── install_dependencies.sh │ ├── change_firewall_settings.sh │ └── start_hotspot.sh ├── ATTRIBUTIONS.md ├── .deepsource.toml ├── src ├── types.d.ts ├── lib │ └── SleepManager.ts ├── signalIcons.tsx ├── components │ └── SubnetDhcpInput.tsx ├── banned_devices.tsx ├── wifi_settings.tsx └── index.tsx ├── plugin.json ├── tsconfig.json ├── .gitignore ├── package.json ├── README.md ├── decky.pyi ├── main.py ├── LICENSE └── pnpm-lock.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtlnetwork/muon/HEAD/assets/logo.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import deckyPlugin from "@decky/rollup"; 2 | 3 | export default deckyPlugin({ 4 | // Add your extra Rollup options here 5 | }) -------------------------------------------------------------------------------- /defaults/assets/remove_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm /var/lib/extensions/muon.raw 3 | systemd-sysext refresh 4 | systemctl restart NetworkManager -------------------------------------------------------------------------------- /ATTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | **Special thanks to the following people:** 2 | * Travis Lane (@Tormak9970) and @jessebofill for their SleepManager implementation (as used in: https://github.com/Tormak9970/TabMaster) -------------------------------------------------------------------------------- /defaults/assets/restart_network_services.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Restarting network services..." 4 | 5 | sudo systemctl restart NetworkManager 6 | sudo systemctl restart iwd 7 | 8 | if [ $? -eq 0 ]; then 9 | exit 0 10 | else 11 | exit 1 12 | fi -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | 6 | [analyzers.meta] 7 | runtime_version = "3.x.x" 8 | 9 | [[analyzers]] 10 | name = "shell" 11 | 12 | [[analyzers]] 13 | name = "javascript" 14 | 15 | [analyzers.meta] 16 | plugins = ["react"] 17 | environment = ["nodejs"] -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.png" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.jpg" { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Muon", 3 | "author": "Jenomorph/WTLNetwork", 4 | "flags": ["root"], 5 | "api_version": 1, 6 | "publish": { 7 | "tags": ["adhoc", "wifi", "p2p", "multiplayer", "root"], 8 | "description": "Ad-hoc WiFi networking plugin for Decky.", 9 | "image": "https://raw.githubusercontent.com/wtlnetwork/muon/refs/heads/main/assets/logo.png" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/SleepManager.ts: -------------------------------------------------------------------------------- 1 | import { findModuleExport } from "@decky/ui"; 2 | 3 | interface SleepManager { 4 | RegisterForNotifyResumeFromSuspend: ( 5 | cb: () => void 6 | ) => { unregister: () => void }; 7 | RegisterForNotifyRequestSuspend: ( 8 | cb: () => void 9 | ) => { unregister: () => void }; 10 | } 11 | 12 | export const sleepManager = findModuleExport( 13 | (exp) => 14 | exp && 15 | typeof exp === "object" && 16 | typeof exp.RegisterForNotifyResumeFromSuspend === "function" && 17 | typeof exp.RegisterForNotifyRequestSuspend === "function" 18 | ) as SleepManager | undefined; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "outDir": "dist", 5 | "module": "ESNext", 6 | "target": "ES2020", 7 | "jsx": "react", 8 | "jsxFactory": "window.SP_REACT.createElement", 9 | "jsxFragmentFactory": "window.SP_REACT.Fragment", 10 | "declaration": false, 11 | "moduleResolution": "node", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "esModuleInterop": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "strict": true, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules"] 23 | } -------------------------------------------------------------------------------- /defaults/assets/extract_network_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Obtain WiFi interface from shell arguments 4 | WIFI_INTERFACE=$1 5 | 6 | # Extract IP Address 7 | IP_ADDRESS=$(ip addr show "$WIFI_INTERFACE" | grep -oP 'inet \K[\d.]+') 8 | 9 | # Extract default gateway 10 | GATEWAY=$(ip route show default | awk '/default/ {print $3}') 11 | 12 | # Extract DNS server settings 13 | if command -v resolvectl > /dev/null; then 14 | DNS_SERVERS=$(resolvectl status | grep -oP 'DNS Servers: \K[\d. ]+' | tr ' ' ',') 15 | else 16 | DNS_SERVERS=$(grep -oP 'nameserver \K[\d.]+' /etc/resolv.conf | tr '\n' ',' | sed 's/,$//') 17 | fi 18 | 19 | # Output results in key=value format for easy parsing in Python 20 | echo "IP_ADDRESS=$IP_ADDRESS" 21 | echo "GATEWAY=$GATEWAY" 22 | echo "DNS_SERVERS=$DNS_SERVERS" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Coverage reports 17 | coverage 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | .pnpm-store 26 | 27 | # Editors 28 | .idea 29 | *.iml 30 | 31 | # OS metadata 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Ignore built ts files 36 | dist/ 37 | 38 | __pycache__/ 39 | 40 | /.yalc 41 | yalc.lock 42 | 43 | .vscode/settings.json 44 | 45 | # Ignore output folder 46 | 47 | backend/out 48 | 49 | # Make sure to ignore any instance of the loader's decky_plugin.py 50 | decky_plugin.py 51 | 52 | # Ignore decky CLI for building plugins 53 | out 54 | out/* 55 | cli/ 56 | cli/* 57 | cli/decky 58 | 59 | release/ 60 | help/ -------------------------------------------------------------------------------- /defaults/assets/stop_hotspot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WIFI_INTERFACE=$1 4 | ORIGINAL_IP=$2 5 | ORIGINAL_GATEWAY=$3 6 | ORIGINAL_DNS=$4 7 | AP_IF="muon0" 8 | 9 | echo "Restoring network configuration for $WIFI_INTERFACE..." 10 | 11 | # Stop the hostapd and dnsmasq services 12 | echo "Stopping hostapd and dnsmasq..." 13 | sudo pkill -x hostapd 14 | sudo pkill -x dnsmasq 15 | sudo rm -f /var/run/hostapd/* 16 | 17 | # Remove the virtual AP interface if it exists 18 | if ip link show "$AP_IF" >/dev/null 2>&1; then 19 | echo "Removing AP interface $AP_IF..." 20 | sudo ip link set "$AP_IF" down 21 | sudo iw dev "$AP_IF" del 22 | fi 23 | 24 | # Restart network services 25 | echo "Restarting NetworkManager and iwd..." 26 | sudo systemctl restart NetworkManager 27 | sudo systemctl restart iwd 28 | 29 | # Bring the main Wi-Fi interface back up 30 | sudo ip link set "$WIFI_INTERFACE" up 31 | 32 | echo "Network configuration restored successfully." -------------------------------------------------------------------------------- /defaults/assets/start_dhcp_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DNSMASQ_CONFIG="/tmp/dnsmasq-hotspot.conf" 3 | DNSMASQ_LOG="/var/log/dnsmasq.log" 4 | INTERFACE=$1 5 | DHCP_RANGE=$2 6 | IP_ADDRESS=$3 7 | 8 | # Remove old dnsmasq configuration. 9 | if [ -f "$DNSMASQ_CONFIG" ]; then 10 | echo "Removing old dnsmasq config at $DNSMASQ_CONFIG..." 11 | sudo rm "$DNSMASQ_CONFIG" 12 | fi 13 | 14 | # Generate new dnsmasq config. 15 | echo "Generating new dnsmasq config..." 16 | cat < "$DNSMASQ_CONFIG" 17 | interface=$INTERFACE 18 | bind-dynamic 19 | dhcp-range=$DHCP_RANGE 20 | dhcp-option=3,$IP_ADDRESS # Gateway 21 | dhcp-option=6,1.1.1.1,8.8.8.8 # DNS for clients 22 | port=0 # Disable DNS serving 23 | log-dhcp 24 | log-facility=$DNSMASQ_LOG # Save logs here 25 | dhcp-broadcast 26 | EOT 27 | echo "dnsmasq config generated successfully." 28 | 29 | # Stop any running copies of dnsmasq to avoid conflicts. 30 | echo "Stopping any existing dnsmasq instances..." 31 | sudo pkill dnsmasq 32 | 33 | # Start dnsmasq 34 | echo "Starting dnsmasq..." 35 | sudo dnsmasq -C "$DNSMASQ_CONFIG" 2>&1 & 36 | 37 | # Pause for two seconds to allow dnsmasq to start. 38 | sleep 1 39 | 40 | # Check if dnsmasq is running 41 | pgrep dnsmasq > /dev/null 42 | 43 | # If dnsmasq is running, exit successfully. Otherwise, exit with an error. 44 | if [ $? -eq 0 ]; then 45 | echo "dnsmasq is running." 46 | exit 0 47 | else 48 | echo "Failed to start dnsmasq." 49 | exit 1 50 | fi -------------------------------------------------------------------------------- /defaults/assets/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | SYSEXT_DIR="./muon" 5 | SYSEXT_RAW="./muon.raw" 6 | SYSEXT_DESTINATION="/var/lib/extensions/muon.raw" 7 | SYSEXT_RELEASE="${SYSEXT_DIR}/usr/lib/extension-release.d/extension-release.muon" 8 | 9 | OS_ID="ID=steamos" 10 | VERSION_ID=$(grep VERSION_ID /etc/os-release) 11 | 12 | # Determine whether to rebuild 13 | SHOULD_REBUILD=false 14 | 15 | if [ ! -f "$SYSEXT_RELEASE" ]; then 16 | echo "No extension-release file found. Rebuilding." 17 | SHOULD_REBUILD=true 18 | else 19 | if ! grep -q "$OS_ID" "$SYSEXT_RELEASE" || ! grep -q "$VERSION_ID" "$SYSEXT_RELEASE"; then 20 | echo "OS version mismatch in extension-release. Rebuilding." 21 | SHOULD_REBUILD=true 22 | fi 23 | fi 24 | 25 | if [ "$SHOULD_REBUILD" = true ]; then 26 | echo "Cleaning up old build (if any)..." 27 | rm -rf "$SYSEXT_DIR" 28 | rm -f "$SYSEXT_RAW" 29 | 30 | mkdir -p "$SYSEXT_DIR" 31 | 32 | for pkg in ../bin/*.pkg.tar.zst; do 33 | tar --use-compress-program=unzstd -xf "$pkg" -C "$SYSEXT_DIR" 34 | done 35 | 36 | mkdir -p "$(dirname "$SYSEXT_RELEASE")" 37 | echo "$OS_ID" > "$SYSEXT_RELEASE" 38 | echo "$VERSION_ID" >> "$SYSEXT_RELEASE" 39 | 40 | chown -R root:root "$SYSEXT_DIR" 41 | 42 | echo "Creating squashfs image..." 43 | mksquashfs "$SYSEXT_DIR" "$SYSEXT_RAW" -comp zstd -all-root -noappend 44 | else 45 | echo "Existing extension-release matches OS version. Skipping rebuild." 46 | fi 47 | 48 | # Link the .raw to system extension dir 49 | mkdir -p /var/lib/extensions 50 | if [ ! -f "$SYSEXT_DESTINATION" ]; then 51 | ln -s "$PWD/muon.raw" "$SYSEXT_DESTINATION" 52 | else 53 | echo "Extension already linked." 54 | fi 55 | 56 | # Enable and refresh systemd-sysext 57 | if ! systemctl is-active --quiet systemd-sysext; then 58 | systemctl enable systemd-sysext 59 | systemctl start systemd-sysext 60 | fi 61 | 62 | systemd-sysext refresh 63 | 64 | echo "Dependencies installed successfully and extension linked." -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muon", 3 | "version": "0.2.1", 4 | "description": "Ad-hoc WiFi networking plugin for Decky.", 5 | "type": "module", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "watch": "rollup -c -w", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/wtlnetwork/muon.git" 14 | }, 15 | "keywords": [ 16 | "decky", 17 | "plugin", 18 | "steam-deck", 19 | "deck", 20 | "ad-hoc", 21 | "wifi", 22 | "hotspot", 23 | "tethering", 24 | "networking" 25 | ], 26 | "author": "Jenomorph/WTLNetwork", 27 | "license": "BSD-3-Clause", 28 | "bugs": { 29 | "url": "https://github.com/wtlnetwork/muon/issues" 30 | }, 31 | "homepage": "https://github.com/wtlnetwork/muon", 32 | "devDependencies": { 33 | "@decky/rollup": "^1.0.2", 34 | "@decky/ui": "^4.11.0", 35 | "@types/react": "18.3.3", 36 | "@types/react-dom": "18.3.0", 37 | "@types/webpack": "^5.28.5", 38 | "rollup": "^4.22.5", 39 | "typescript": "^5.6.2" 40 | }, 41 | "dependencies": { 42 | "@decky/api": "^1.1.3", 43 | "react-icons": "^5.3.0", 44 | "tslib": "^2.7.0" 45 | }, 46 | "pnpm": { 47 | "peerDependencyRules": { 48 | "ignoreMissing": [ 49 | "react", 50 | "react-dom" 51 | ] 52 | } 53 | }, 54 | "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af", 55 | "remote_binary_bundling": false, 56 | "remote_binary": 57 | [ 58 | { 59 | "name": "dnsmasq-2.91-1-x86_64.pkg.tar.zst", 60 | "url": "https://steamdeck-packages.steamos.cloud/archlinux-mirror/extra-main/os/x86_64/dnsmasq-2.91-1-x86_64.pkg.tar.zst", 61 | "sha256hash": "7706d35ea76993c8e10068eb4190ef25e7d712c054a2b5df267848d71a41f650" 62 | }, 63 | { 64 | "name": "hostapd-2.11-4-x86_64.pkg.tar.zst", 65 | "url": "https://steamdeck-packages.steamos.cloud/archlinux-mirror/extra-main/os/x86_64/hostapd-2.11-4-x86_64.pkg.tar.zst", 66 | "sha256hash": "5085046d71c49f741218b10a19e1eaa3bb25a337fd6829018982a72535903d3d" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /defaults/assets/change_firewall_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Full IP Address (e.g. 192.168.8.1) 4 | IP_ADDRESS=$1 5 | 6 | echo "Configuring firewalld for IP address: $IP_ADDRESS" 7 | 8 | # Extract subnet from IP Address (e.g. 192.168.8.1 -> 192.168.8.0) 9 | SUBNET=$(echo "$IP_ADDRESS" | sed 's/\.[0-9]\+$/\.0/') 10 | echo "Extracted Subnet: $SUBNET/24" 11 | 12 | # Check if firewalld is active 13 | FIREWALLD_STATUS=$(sudo systemctl is-active firewalld) 14 | echo "Firewalld status: $FIREWALLD_STATUS" 15 | 16 | if [ "$FIREWALLD_STATUS" != "active" ]; then 17 | echo "Firewalld is not active. Exiting." 18 | exit 1 19 | fi 20 | 21 | # Get the active zone, default to 'public' if not found 22 | ACTIVE_ZONE=$(sudo firewall-cmd --get-active-zones | awk 'NR==1{print $1}') 23 | if [ -z "$ACTIVE_ZONE" ]; then 24 | echo "Could not determine the active firewalld zone. Using default: public" 25 | ACTIVE_ZONE="public" 26 | fi 27 | echo "Using firewalld zone: $ACTIVE_ZONE" 28 | 29 | # Allow broadcast traffic and UDP for server discovery 30 | echo "Allowing broadcast traffic for server discovery..." 31 | sudo firewall-cmd --zone="$ACTIVE_ZONE" --add-rich-rule="rule family=ipv4 destination address=255.255.255.255 protocol value=udp accept" --permanent 32 | sudo firewall-cmd --zone="$ACTIVE_ZONE" --add-rich-rule="rule family=ipv4 destination address=255.255.255.255 protocol value=tcp accept" --permanent 33 | sudo firewall-cmd --zone="$ACTIVE_ZONE" --add-rich-rule="rule family=ipv4 source address=${SUBNET}/24 protocol value=udp accept" --permanent 34 | 35 | # Allow DHCP service 36 | echo "Allowing DHCP traffic..." 37 | sudo firewall-cmd --zone="$ACTIVE_ZONE" --add-service=dhcp --permanent 38 | 39 | # Check if the muon0 interface is in the active zone and add it if it isn't 40 | if ! sudo firewall-cmd --zone="$ACTIVE_ZONE" --list-interfaces | grep -qw muon0; then 41 | echo "Adding muon0 to zone $ACTIVE_ZONE..." 42 | sudo firewall-cmd --zone="$ACTIVE_ZONE" --add-interface=muon0 --permanent 43 | fi 44 | 45 | # Reload firewalld to apply changes 46 | echo "Reloading firewalld to apply changes..." 47 | FIREWALLD_RELOAD=$(sudo firewall-cmd --reload) 48 | 49 | if [ "$FIREWALLD_RELOAD" == "success" ]; then 50 | echo "Firewalld configuration updated successfully." 51 | exit 0 52 | else 53 | echo "Failed to reload firewalld." 54 | exit 1 55 | fi 56 | -------------------------------------------------------------------------------- /src/signalIcons.tsx: -------------------------------------------------------------------------------- 1 | export const getSignalIcon = (signalStrength: number, size: number = 50) => { 2 | const iconStyle = { 3 | width: `${size}px`, 4 | height: `${size}px`, 5 | }; 6 | 7 | if (signalStrength >= -49) { 8 | return ( 9 | Excellent Signal 14 | ); 15 | } else if (signalStrength >= -59) { 16 | return ( 17 | Good Signal 22 | ); 23 | } else if (signalStrength >= -69) { 24 | return ( 25 | Fair Signal 30 | ); 31 | } else { 32 | return ( 33 | Weak Signal 38 | ); 39 | } 40 | }; -------------------------------------------------------------------------------- /src/components/SubnetDhcpInput.tsx: -------------------------------------------------------------------------------- 1 | import { PanelSectionRow, TextField, Field } from "@decky/ui"; 2 | import { useState } from "react"; 3 | 4 | interface SubnetDhcpInputProps { 5 | baseIp: string; 6 | onChange: (fullBaseIp: string, dhcpStart: string, dhcpEnd: string) => void; 7 | error?: string; 8 | } 9 | 10 | export const SubnetDhcpInput = ({ baseIp, onChange, error }: SubnetDhcpInputProps) => { 11 | const [octets, setOctets] = useState(baseIp.split(".").map((o) => o || "")); 12 | const [start, setStart] = useState("100"); 13 | const [end, setEnd] = useState("200"); 14 | 15 | const handleOctetChange = (index: number, value: string) => { 16 | if (!/^[0-9]{0,3}$/.test(value)) return; 17 | const val = Math.min(255, parseInt(value || "0")); 18 | const newOctets = [...octets]; 19 | newOctets[index] = val.toString(); 20 | setOctets(newOctets); 21 | onChange( 22 | newOctets.join("."), 23 | `${newOctets.slice(0, 3).join(".")}.${start}`, 24 | `${newOctets.slice(0, 3).join(".")}.${end}` 25 | ); 26 | }; 27 | 28 | const handleRangeChange = (setter: (v: string) => void, value: string, which: "start" | "end") => { 29 | if (!/^[0-9]{0,3}$/.test(value)) return; 30 | const val = Math.min(255, parseInt(value || "0")); 31 | setter(val.toString()); 32 | onChange( 33 | octets.join("."), 34 | `${octets.slice(0, 3).join(".")}.${which === "start" ? val : start}`, 35 | `${octets.slice(0, 3).join(".")}.${which === "end" ? val : end}` 36 | ); 37 | }; 38 | 39 | return ( 40 | <> 41 | 42 | 43 |
44 | {octets.slice(0, 3).flatMap((octet, i) => [ 45 | handleOctetChange(i, e.target.value)} 53 | />, 54 | i < 2 && . 55 | ])} 56 | . 57 | handleOctetChange(3, e.target.value)} 64 | /> 65 |
66 |
67 |
68 | 69 | 70 |
71 | {octets[0]} 72 | . 73 | {octets[1]} 74 | . 75 | {octets[2]} 76 | . 77 | handleRangeChange(setStart, e.target.value, "start")} 84 | /> 85 | - 86 | handleRangeChange(setEnd, e.target.value, "end")} 93 | /> 94 |
95 |
96 |
97 | {error && ( 98 | 99 |

{error}

100 |
101 | )} 102 | 103 | ); 104 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Muon - Ad-Hoc connection for the Steam Deck** 2 | 3 | A decky-loader plugin that creates a WiFi access point for another Devices to connect. 4 | 5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | **List of Features:** 23 | * 1 Steam Deck needs to download the plugin for hosting; guests are not required to do so. 24 | * Random host password generation per device restart (option to keep it static) 25 | * Toaster notifications for joined & disconnected devices 26 | * Option to change hostname for the Steam Deck 27 | * Option for credentials being remembered for quick access 28 | 29 | **[BE UP-TO-DATE WITH THE NEWEST RELEASES!](https://github.com/wtlnetwork/muon/releases)** 30 | 31 | ## Preview: 32 | __Here are a sample of games verified for Muon!__ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
GARRY'S MODRAINBOW SIX: VEGAS 2
Placeholder GIFPlaceholder GIF
Placeholder GIFPlaceholder GIF
QUAKE III ARENAStardew Valley (Coming Soon)
56 | 57 | 58 | 59 | ## Join the Steam Group for Verified games! 60 | 61 | Official Muon Steam Group: 62 | * [Muon Verified](https://steamcommunity.com/groups/muonverified) 63 | 64 | **Need help setting up Muon? Here is the Wiki page to assist you!** 65 | * [Muon Wiki](https://github.com/wtlnetwork/muon/wiki) 66 | 67 | ## Planned Updates: 68 | 69 | * decky-loader plugin store release (0.1.6) ✔ 70 | * QoL & Bug Fixes based of from feedback (0.1.7+) 71 | * RemotePlayWhatever & LAN over USB-C Cable Integration (0.4.0)? 72 | 73 |

⚠️ PRECAUTION ⚠️

74 | 75 | 76 | 77 | 86 | 87 |
78 | WARNING: The following actions are strongly discouraged. We assume no liability for any consequences resulting from these actions: 79 |
    80 |
  • Connecting more than two Steam Decks via Ad-Hoc (untested, subject to change based on research and feedback)
  • 81 |
  • Using Muon during flights or air travel
  • 82 |
  • Sharing your passphrase with strangers
  • 83 |
  • Running Muon with hardware-intensive games (risk of overloading the host Steam Deck)
  • 84 |
85 |
88 | 89 | 90 | ## Libraries Used & Credits 91 | Built using the Decky Plugin Template [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://deckbrew.xyz/discord) as a basis. 92 | 93 | Animated Wi-Fi Logo by: Esko Ahonen 94 | 95 | Animated Steam Deck by: Valve Software 96 | 97 | Tormak9970 - Used Deck-P2P as a reference point. 98 | 99 | steve228uk - Used TunnelDeck as reference for installing dependencies locally 100 | 101 | @Tormak9970 and @jessebofill - Used code from [TabMaster](https://github.com/Tormak9970/TabMaster) to implement sleep management after a SteamOS change broke it. 102 | 103 | ## Project members 104 | **Jenomorph** - Project lead / Bug Tester / Game Verifier 105 | 106 | **WTLNetwork** - Developer 107 | 108 | -------------------------------------------------------------------------------- /src/banned_devices.tsx: -------------------------------------------------------------------------------- 1 | import { ModalRoot, showModal } from "@decky/ui"; 2 | import { useEffect, useState } from "react"; 3 | import { ButtonItem, PanelSectionRow } from "@decky/ui"; 4 | import { FaTrash, FaTimes } from "react-icons/fa"; 5 | import { callable, toaster } from "@decky/api"; 6 | 7 | // Function to show the banned devices modal 8 | export const showBannedDevicesModal = () => { 9 | showModal(, undefined, { strTitle: "Banned Devices" }); 10 | }; 11 | 12 | export const BootIcon = () => { 13 | return ( 14 | Ban User 19 | ); 20 | }; 21 | 22 | // Main modal component 23 | const BannedDevicesModal = ({ closeModal }: { closeModal?: () => void }) => { 24 | const [bannedDevices, setBannedDevices] = useState([]); 25 | const [loading, setLoading] = useState(true); 26 | 27 | const retrieveBanList = callable<[], string[]>("retrieve_ban_list"); 28 | const unbanMacAddress = callable<[string], boolean>("unban_mac_address"); 29 | 30 | // Fetch banned MAC addresses 31 | useEffect(() => { 32 | const fetchBannedDevices = async () => { 33 | try { 34 | const result = await retrieveBanList(); 35 | setBannedDevices(result); 36 | } catch (error) { 37 | toaster.toast({ title: "Error", body: "Failed to retrieve banned devices." }); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | fetchBannedDevices(); 44 | }, []); 45 | 46 | // Handle unban action 47 | const handleUnban = async (mac: string) => { 48 | const success = await unbanMacAddress(mac); 49 | if (success) { 50 | toaster.toast({ title: "Success", body: `Unbanned ${mac}` }); 51 | setBannedDevices((prev) => prev.filter((item) => item !== mac)); 52 | } else { 53 | toaster.toast({ title: "Error", body: `Failed to unban ${mac}` }); 54 | } 55 | }; 56 | 57 | return ( 58 | 59 | {loading ? ( 60 |

Loading banned devices...

61 | ) : bannedDevices.length > 0 ? ( 62 | bannedDevices.map((mac) => ( 63 | 64 |
65 | {mac} 66 | handleUnban(mac)}> 67 | Unban 68 | 69 |
70 |
71 | )) 72 | ) : ( 73 |

No banned devices found.

74 | )} 75 | 76 | {/* Close button at the bottom */} 77 | 78 | 79 | Close 80 | 81 | 82 |
83 | ); 84 | }; -------------------------------------------------------------------------------- /defaults/assets/start_hotspot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Input Parameters 4 | WIFI_INTERFACE=$1 5 | STATIC_IP=$2 6 | SSID=$3 7 | PASSPHRASE=$4 8 | HOSTAPD_CONF="/tmp/hostapd.conf" 9 | CTRL_INTERFACE_DIR="/var/run/hostapd" 10 | 11 | echo "Starting hotspot setup..." 12 | echo "WiFi Interface: $WIFI_INTERFACE" 13 | echo "Static IP: $STATIC_IP" 14 | echo "SSID: $SSID" 15 | echo "Passphrase: $PASSPHRASE" 16 | 17 | AP_IF="muon0" 18 | 19 | PHY=$(iw dev "$WIFI_INTERFACE" info 2>/dev/null | awk '/wiphy/ {print "phy"$2}') 20 | if [ -z "$PHY" ]; then 21 | echo "Error: Unable to determine the PHY for interface $WIFI_INTERFACE." 22 | exit 1 23 | fi 24 | echo "Using PHY $PHY to create $AP_IF" 25 | 26 | # Step 1: Stop Network Services 27 | echo "Stopping network services..." 28 | sudo systemctl stop NetworkManager 29 | sudo systemctl stop iwd 30 | if [ $? -ne 0 ]; then 31 | echo "Failed to stop network services." 32 | exit 1 33 | fi 34 | echo "Network services stopped." 35 | 36 | # Step 2: Create AP interface (muon0) and configure static IP there 37 | echo "Setting static IP for $AP_IF on $PHY" 38 | 39 | # If muon0 already exists from a previous run, remove it cleanly 40 | if ip link show "$AP_IF" >/dev/null 2>&1; then 41 | echo "$AP_IF already exists; deleting it first..." 42 | sudo iw dev "$AP_IF" del || true 43 | sleep 0.5 44 | fi 45 | 46 | echo "Bringing down $WIFI_INTERFACE to prepare for $AP_IF..." 47 | sudo ip link set "$WIFI_INTERFACE" down 48 | 49 | # Create a new virtual interface on the same PHY 50 | if ! sudo iw phy "$PHY" interface add "$AP_IF" type __ap; then 51 | echo "Failed to create $AP_IF on $PHY." 52 | exit 1 53 | fi 54 | 55 | # Bring it up 56 | sudo ip link set "$AP_IF" up 57 | 58 | # Assign the static IP to muon0 59 | echo "Assigning IP $STATIC_IP/24 to $AP_IF..." 60 | sudo ip addr flush dev "$AP_IF" || true 61 | sudo ip addr add "$STATIC_IP/24" dev "$AP_IF" 62 | sleep 1 63 | 64 | # Validate the IP assignment 65 | echo "Validating IP assignment on $AP_IF..." 66 | FINAL_IP_CHECK=$(ip addr show "$AP_IF" | grep -oP 'inet \K[\d.]+') 67 | if [ "$FINAL_IP_CHECK" != "$STATIC_IP" ]; then 68 | echo "Failed to assign IP $STATIC_IP to $AP_IF." 69 | exit 1 70 | else 71 | echo "Successfully assigned IP $STATIC_IP to $AP_IF." 72 | fi 73 | 74 | # Step 3: Prepare Control Interface Directory 75 | echo "Ensuring control interface directory exists..." 76 | 77 | # Create the control directory if it doesn't exist 78 | if [ ! -d "$CTRL_INTERFACE_DIR" ]; then 79 | echo "Creating control interface directory: $CTRL_INTERFACE_DIR" 80 | sudo mkdir -p "$CTRL_INTERFACE_DIR" 81 | fi 82 | 83 | # Set the correct permissions 84 | echo "Setting correct permissions for control interface directory..." 85 | sudo chown root:root "$CTRL_INTERFACE_DIR" 86 | sudo chmod 755 "$CTRL_INTERFACE_DIR" 87 | echo "Control interface directory is ready." 88 | 89 | echo "Checking if hostapd.deny file exists..." 90 | 91 | # Create an empty deny file if it doesn't exist 92 | mkdir -p /etc/hostapd && touch /etc/hostapd/hostapd.deny 93 | 94 | # Step 4: Start Hotspot 95 | echo "Starting hotspot with SSID: $SSID" 96 | 97 | # Generate hostapd configuration 98 | echo "Generating hostapd configuration..." 99 | cat < /dev/null 100 | interface=$AP_IF 101 | driver=nl80211 102 | ssid=$SSID 103 | hw_mode=g 104 | channel=6 105 | wpa=2 106 | wpa_passphrase=$PASSPHRASE 107 | wpa_key_mgmt=WPA-PSK 108 | rsn_pairwise=CCMP 109 | 110 | # Control interface for hostapd_cli communication 111 | ctrl_interface=$CTRL_INTERFACE_DIR 112 | ctrl_interface_group=0 113 | deny_mac_file=/etc/hostapd/hostapd.deny 114 | EOT 115 | echo "Hostapd configuration generated." 116 | 117 | # Start hostapd 118 | echo "Starting hostapd..." 119 | sudo hostapd $HOSTAPD_CONF -B 120 | if [ $? -ne 0 ]; then 121 | echo "Failed to start hostapd." 122 | exit 1 123 | fi 124 | echo "Hotspot started successfully." 125 | 126 | # Step 5: Verify Hostapd Control Interface 127 | echo "Verifying hostapd control interface..." 128 | 129 | # Check if the control socket was created 130 | if [ -e "$CTRL_INTERFACE_DIR/$AP_IF" ]; then 131 | echo "Control interface socket exists." 132 | else 133 | echo "Control interface socket not found. Check hostapd logs for issues." 134 | exit 1 135 | fi 136 | 137 | # Test hostapd_cli connection 138 | echo "Testing hostapd_cli connection..." 139 | sudo hostapd_cli -p "$CTRL_INTERFACE_DIR" -i "$AP_IF" status 140 | if [ $? -ne 0 ]; then 141 | echo "Failed to connect to hostapd using hostapd_cli." 142 | exit 1 143 | fi 144 | echo "hostapd_cli connection successful." 145 | 146 | echo "Hotspot setup complete." 147 | 148 | exit 0 -------------------------------------------------------------------------------- /src/wifi_settings.tsx: -------------------------------------------------------------------------------- 1 | import { Field, ModalRoot, showModal, Toggle } from "@decky/ui"; 2 | import { useState } from "react"; 3 | import { ButtonItem, PanelSectionRow, TextField } from "@decky/ui"; 4 | import { FaCheck, FaTimes, FaBan } from "react-icons/fa"; 5 | import { callable, toaster } from "@decky/api"; 6 | import { showBannedDevicesModal } from "./banned_devices"; 7 | import { SubnetDhcpInput } from "./components/SubnetDhcpInput"; 8 | 9 | export const showWifiSettingsModal = ( 10 | currentSsid: string, 11 | currentPassphrase: string, 12 | alwaysUseStoredCredentials: boolean, 13 | currentBaseIp: string, 14 | currentDhcpStart: string, 15 | currentDhcpEnd: string, 16 | onSave: (ssid: string, passphrase: string, alwaysUse: boolean, ip: string, dhcpStart: string, dhcpEnd: string) => void 17 | ) => { 18 | showModal( 19 | , 28 | undefined, 29 | { strTitle: "Edit WiFi Settings" } 30 | ); 31 | }; 32 | 33 | const WifiSettingsModal = ({ 34 | currentSsid, 35 | currentPassphrase, 36 | alwaysUseStoredCredentials, 37 | currentBaseIp, 38 | currentDhcpStart, 39 | currentDhcpEnd, 40 | onSave, 41 | closeModal, 42 | }: { 43 | currentSsid: string; 44 | currentPassphrase: string; 45 | alwaysUseStoredCredentials: boolean; 46 | currentBaseIp: string; 47 | currentDhcpStart: string; 48 | currentDhcpEnd: string; 49 | onSave: (ssid: string, passphrase: string, alwaysUse: boolean, ip: string, dhcpStart: string, dhcpEnd: string) => void 50 | closeModal?: () => void; 51 | }) => { 52 | const [newSsid, setNewSsid] = useState(currentSsid); 53 | const [newPassphrase, setNewPassphrase] = useState(currentPassphrase); 54 | const [alwaysUse, setAlwaysUse] = useState(alwaysUseStoredCredentials); 55 | const [error, setError] = useState(null); 56 | const [baseIp, setBaseIp] = useState(currentBaseIp); 57 | const [dhcpStart, setDhcpStart] = useState(currentDhcpStart); 58 | const [dhcpEnd, setDhcpEnd] = useState(currentDhcpEnd); 59 | 60 | const handleSave = async () => { 61 | if (newPassphrase.length < 8 || newPassphrase.length > 63) { 62 | setError("Password must be between 8 and 63 characters."); 63 | return; 64 | } 65 | 66 | setError(null); // Clear previous errors if any 67 | 68 | try { 69 | // Call update_credentials and get the updated values 70 | const updatedConfig = await callable<[string, string, boolean], { ssid: string; passphrase: string; always_use_stored_credentials: boolean }>( 71 | "update_credentials" 72 | )(newSsid, newPassphrase, alwaysUse); 73 | const updateDhcp = callable<[string, string, string], { ip_address: string; dhcp_range: string }>("update_dhcp"); 74 | await updateDhcp(baseIp, dhcpStart, dhcpEnd); 75 | // Update UI with the latest values 76 | onSave( 77 | updatedConfig.ssid, 78 | updatedConfig.passphrase, 79 | updatedConfig.always_use_stored_credentials, 80 | baseIp, 81 | dhcpStart, 82 | dhcpEnd 83 | ); 84 | closeModal?.(); 85 | } catch (error) { 86 | setError("Could not save settings."); 87 | toaster.toast({ title: "Error", body: "Could not save settings." }); 88 | } 89 | }; 90 | 91 | return ( 92 | 93 | 94 | setNewSsid(e.target.value)} /> 95 | 96 | 97 | setNewPassphrase(e.target.value)} 101 | /> 102 | 103 | 104 | 107 | setAlwaysUse(toggleValue)} 110 | /> 111 | 112 | 113 | {error &&

{error}

} 114 | 115 | { 119 | setBaseIp(ip); 120 | setDhcpStart(start); 121 | setDhcpEnd(end); 122 | }} 123 | /> 124 | 125 | 126 | 127 | Manage Banned Devices 128 | 129 | 130 | 131 | 132 | Save 133 | 134 | 135 | Cancel 136 | 137 | 138 |
139 | ); 140 | }; 141 | -------------------------------------------------------------------------------- /decky.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | This module exposes various constants and helpers useful for decky plugins. 3 | 4 | * Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. 5 | * Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. 6 | * Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. 7 | 8 | Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. 9 | 10 | Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. 11 | 12 | A logging facility `logger` is available which writes to the recommended location. 13 | """ 14 | 15 | __version__ = '1.0.0' 16 | 17 | import logging 18 | 19 | from typing import Any 20 | 21 | """ 22 | Constants 23 | """ 24 | 25 | HOME: str 26 | """ 27 | The home directory of the effective user running the process. 28 | Environment variable: `HOME`. 29 | If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. 30 | e.g.: `/home/deck` 31 | """ 32 | 33 | USER: str 34 | """ 35 | The effective username running the process. 36 | Environment variable: `USER`. 37 | It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. 38 | e.g.: `deck` 39 | """ 40 | 41 | DECKY_VERSION: str 42 | """ 43 | The version of the decky loader. 44 | Environment variable: `DECKY_VERSION`. 45 | e.g.: `v2.5.0-pre1` 46 | """ 47 | 48 | DECKY_USER: str 49 | """ 50 | The user whose home decky resides in. 51 | Environment variable: `DECKY_USER`. 52 | e.g.: `deck` 53 | """ 54 | 55 | DECKY_USER_HOME: str 56 | """ 57 | The home of the user where decky resides in. 58 | Environment variable: `DECKY_USER_HOME`. 59 | e.g.: `/home/deck` 60 | """ 61 | 62 | DECKY_HOME: str 63 | """ 64 | The root of the decky folder. 65 | Environment variable: `DECKY_HOME`. 66 | e.g.: `/home/deck/homebrew` 67 | """ 68 | 69 | DECKY_PLUGIN_SETTINGS_DIR: str 70 | """ 71 | The recommended path in which to store configuration files (created automatically). 72 | Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. 73 | e.g.: `/home/deck/homebrew/settings/decky-plugin-template` 74 | """ 75 | 76 | DECKY_PLUGIN_RUNTIME_DIR: str 77 | """ 78 | The recommended path in which to store runtime data (created automatically). 79 | Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. 80 | e.g.: `/home/deck/homebrew/data/decky-plugin-template` 81 | """ 82 | 83 | DECKY_PLUGIN_LOG_DIR: str 84 | """ 85 | The recommended path in which to store persistent logs (created automatically). 86 | Environment variable: `DECKY_PLUGIN_LOG_DIR`. 87 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template` 88 | """ 89 | 90 | DECKY_PLUGIN_DIR: str 91 | """ 92 | The root of the plugin's directory. 93 | Environment variable: `DECKY_PLUGIN_DIR`. 94 | e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` 95 | """ 96 | 97 | DECKY_PLUGIN_NAME: str 98 | """ 99 | The name of the plugin as specified in the 'plugin.json'. 100 | Environment variable: `DECKY_PLUGIN_NAME`. 101 | e.g.: `Example Plugin` 102 | """ 103 | 104 | DECKY_PLUGIN_VERSION: str 105 | """ 106 | The version of the plugin as specified in the 'package.json'. 107 | Environment variable: `DECKY_PLUGIN_VERSION`. 108 | e.g.: `0.0.1` 109 | """ 110 | 111 | DECKY_PLUGIN_AUTHOR: str 112 | """ 113 | The author of the plugin as specified in the 'plugin.json'. 114 | Environment variable: `DECKY_PLUGIN_AUTHOR`. 115 | e.g.: `John Doe` 116 | """ 117 | 118 | DECKY_PLUGIN_LOG: str 119 | """ 120 | The path to the plugin's main logfile. 121 | Environment variable: `DECKY_PLUGIN_LOG`. 122 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` 123 | """ 124 | 125 | """ 126 | Migration helpers 127 | """ 128 | 129 | 130 | def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: 131 | """ 132 | Migrate files and directories to a new location and remove old locations. 133 | Specified files will be migrated to `target_dir`. 134 | Specified directories will have their contents recursively migrated to `target_dir`. 135 | 136 | Returns the mapping of old -> new location. 137 | """ 138 | 139 | 140 | def migrate_settings(*files_or_directories: str) -> dict[str, str]: 141 | """ 142 | Migrate files and directories relating to plugin settings to the recommended location and remove old locations. 143 | Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 144 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 145 | 146 | Returns the mapping of old -> new location. 147 | """ 148 | 149 | 150 | def migrate_runtime(*files_or_directories: str) -> dict[str, str]: 151 | """ 152 | Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations 153 | Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 154 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 155 | 156 | Returns the mapping of old -> new location. 157 | """ 158 | 159 | 160 | def migrate_logs(*files_or_directories: str) -> dict[str, str]: 161 | """ 162 | Migrate files and directories relating to plugin logs to the recommended location and remove old locations. 163 | Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. 164 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. 165 | 166 | Returns the mapping of old -> new location. 167 | """ 168 | 169 | 170 | """ 171 | Logging 172 | """ 173 | 174 | logger: logging.Logger 175 | """The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" 176 | 177 | """ 178 | Event handling 179 | """ 180 | # TODO better docstring im lazy 181 | async def emit(event: str, *args: Any) -> None: 182 | """ 183 | Send an event to the frontend. 184 | """ -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonItem, 3 | DialogButton, 4 | Focusable, 5 | PanelSection, 6 | PanelSectionRow, 7 | TextField, 8 | staticClasses, 9 | } from "@decky/ui"; 10 | import { 11 | callable, 12 | definePlugin, 13 | toaster, 14 | call, 15 | addEventListener, 16 | removeEventListener 17 | } from "@decky/api"; 18 | import { useState, useEffect } from "react"; 19 | import { FaWifi, FaSpinner, FaCog } from "react-icons/fa"; 20 | import { showWifiSettingsModal } from "./wifi_settings"; 21 | import { getSignalIcon } from "./signalIcons"; 22 | import { BootIcon } from "./banned_devices"; 23 | import { sleepManager } from "./lib/SleepManager"; 24 | 25 | const startHotspot = callable<[], void>("start_hotspot"); 26 | const stopHotspot = callable<[], void>("stop_hotspot"); 27 | const checkDependencies = callable<[], Record>("check_dependencies"); 28 | const isHotspotActive = callable<[], boolean>("is_hotspot_active"); 29 | const updateCredentials = callable<[string, string, boolean], void>("update_credentials"); 30 | const installDependencies = callable<[], { success: boolean; error?: string }>("install_dependencies"); 31 | const getConnectedDevices = callable<[], any>("get_connected_devices"); 32 | const kickMac = callable<[string], boolean>("kick_mac"); 33 | const getIpAddress = callable<[], string>("get_ip_address"); 34 | 35 | let _muonListenerRegistered = false; 36 | 37 | declare global { 38 | interface Window { 39 | SteamClient: any; 40 | } 41 | } 42 | 43 | function Content() { 44 | const [hotspotStatus, setHotspotStatus] = useState<"running" | "loading" | "stopped">("stopped"); 45 | const [ssid, setSsid] = useState(""); 46 | const [passphrase, setPassphrase] = useState(""); 47 | const [alwaysUseStoredCredentials, setAlwaysUseStoredCredentials] = useState(false); 48 | const [baseIp, setBaseIp] = useState("192.168.8.1"); 49 | const [dhcpStart, setDhcpStart] = useState("192.168.8.100"); 50 | const [dhcpEnd, setDhcpEnd] = useState("192.168.8.200"); 51 | const [dependencies, setDependencies] = useState | null>(null); 52 | const [installingDependencies, setInstallingDependencies] = useState(false); 53 | const [isBlocked, setIsBlocked] = useState(false); 54 | const [connectedDevices, setConnectedDevices] = useState([]); 55 | const [ipAddress, setIpAddress] = useState(""); 56 | 57 | useEffect(() => { 58 | const fetchIp = async () => { 59 | try { 60 | const ip = await getIpAddress(); 61 | setIpAddress(ip); 62 | } catch (err) { 63 | console.error("Failed to fetch IP address", err); 64 | setIpAddress("Unknown"); 65 | } 66 | }; 67 | fetchIp(); 68 | }, []); 69 | 70 | const generateRandomPassword = () => { 71 | const charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789"; 72 | return Array.from({ length: 8 }, () => charset[Math.floor(Math.random() * charset.length)]).join(""); 73 | }; 74 | 75 | const handleKickDevice = async (mac: string) => { 76 | const success = await kickMac(mac); 77 | if (success) { 78 | toaster.toast({ title: "Device Kicked", body: `Successfully kicked ${mac}` }); 79 | } else { 80 | toaster.toast({ title: "Error", body: `Failed to kick ${mac}` }); 81 | } 82 | }; 83 | 84 | useEffect(() => { 85 | const initializeSettings = async () => { 86 | try { 87 | const storedConfig = await callable<[], { 88 | ssid: string; 89 | passphrase: string; 90 | always_use_stored_credentials: boolean; 91 | ip_address: string; 92 | dhcp_range: string; 93 | }>("load_settings")(); 94 | let alwaysUse = storedConfig.always_use_stored_credentials; 95 | let finalSsid = storedConfig.ssid; 96 | let finalPassphrase = storedConfig.passphrase; 97 | 98 | if (!finalSsid || !finalPassphrase || finalSsid === "undefined" || finalPassphrase === "undefined") { 99 | toaster.toast({ title: "Warning", body: "Stored credentials missing! Generating new credentials as failsafe." }); 100 | finalSsid = await callable<[], string>("get_hostname")(); 101 | finalPassphrase = generateRandomPassword(); 102 | await updateCredentials(finalSsid, finalPassphrase, alwaysUse); 103 | } 104 | 105 | setSsid(finalSsid); 106 | setPassphrase(finalPassphrase); 107 | setAlwaysUseStoredCredentials(alwaysUse); 108 | setBaseIp(storedConfig.ip_address); 109 | const [start, end] = storedConfig.dhcp_range.split(",").slice(0, 2); 110 | setDhcpStart(start); 111 | setDhcpEnd(end); 112 | 113 | const deps = await checkDependencies(); 114 | setDependencies(deps); 115 | 116 | const hotspotActive = await isHotspotActive(); 117 | setHotspotStatus(hotspotActive ? "running" : "stopped"); 118 | 119 | const rfkillBlocked = await callable<[], boolean>("is_rfkill_blocking_wlan")(); 120 | setIsBlocked(rfkillBlocked); 121 | 122 | } catch (error) { 123 | toaster.toast({ title: "Error", body: "Failed to initialize settings." }); 124 | console.error("Failed to initialize settings:", error); 125 | } 126 | }; 127 | 128 | initializeSettings(); 129 | }, []); 130 | 131 | useEffect(() => { 132 | const needsInstall = 133 | dependencies && (!dependencies["dnsmasq"] || !dependencies["hostapd"]); 134 | 135 | if (needsInstall && !installingDependencies) { 136 | (async () => { 137 | try { 138 | setInstallingDependencies(true); 139 | toaster.toast({ title: "Installing dependencies", body: "Please wait..." }); 140 | 141 | const result = await installDependencies(); 142 | if (result.success) { 143 | toaster.toast({ title: "Success", body: "Dependencies installed successfully!" }); 144 | 145 | await new Promise(r => setTimeout(r, 1500)); 146 | 147 | const updatedDeps = await checkDependencies(); 148 | setDependencies(updatedDeps); 149 | 150 | const hotspotActive = await isHotspotActive(); 151 | setHotspotStatus(hotspotActive ? "running" : "stopped"); 152 | } else { 153 | const err = (result as any)?.error || (result as any)?.missing?.join(", "); 154 | toaster.toast({ title: "Error", body: `Failed to install: ${err ?? "unknown error"}` }); 155 | } 156 | } finally { 157 | setInstallingDependencies(false); 158 | } 159 | })(); 160 | } 161 | }, [dependencies, installingDependencies]); 162 | 163 | useEffect(() => { 164 | const fetchDevices = async () => { 165 | try { 166 | const devices = await getConnectedDevices(); 167 | // Parse device list safely 168 | const parsedDevices = typeof devices === "string" ? JSON.parse(devices) : devices; 169 | const newDeviceList = (Array.isArray(parsedDevices) ? parsedDevices : []) 170 | .filter(d => d.ip && d.hostname); 171 | 172 | // Store updated device list and the updated announcements 173 | setConnectedDevices(newDeviceList); 174 | 175 | } catch (error) { 176 | console.error("Failed to fetch connected devices:", error); 177 | setConnectedDevices([]); 178 | } 179 | }; 180 | 181 | // Poll every 5 seconds when hotspot is running 182 | if (hotspotStatus === "running") { 183 | fetchDevices(); // Fetch immediately when hotspot starts 184 | 185 | const interval = setInterval(() => { 186 | fetchDevices(); 187 | }, 2000); // Poll every 5 seconds 188 | 189 | return () => clearInterval(interval); 190 | } 191 | 192 | // Explicitly return undefined when not polling 193 | return undefined; 194 | }, [hotspotStatus]); 195 | 196 | 197 | const spinnerStyle = { 198 | animation: "spin 1s linear infinite" 199 | }; 200 | 201 | const Spinner = () => ( 202 | <> 203 | 211 | 212 | 213 | 214 | 215 | ); 216 | 217 | const handleClick = async () => { 218 | if (passphrase.length < 8 || passphrase.length > 63) { 219 | toaster.toast({ title: "Error", body: "Password must be between 8 and 63 characters." }); 220 | return; 221 | } 222 | 223 | setHotspotStatus("loading"); 224 | 225 | try { 226 | if (hotspotStatus === "stopped") { 227 | await startHotspot(); 228 | setHotspotStatus("running"); 229 | toaster.toast({ title: "Hotspot Started", body: `SSID: ${ssid}` }); 230 | } else { 231 | await stopHotspot(); 232 | setHotspotStatus("stopped"); 233 | toaster.toast({ title: "Hotspot Stopped", body: "Hotspot has been disabled." }); 234 | } 235 | } catch (error) { 236 | toaster.toast({ title: "Error", body: "Failed to toggle hotspot." }); 237 | } finally { 238 | const hotspotActive = await isHotspotActive(); 239 | setHotspotStatus(hotspotActive ? "running" : "stopped"); 240 | } 241 | }; 242 | 243 | if (dependencies && (!dependencies["dnsmasq"] || !dependencies["hostapd"])) { 244 | 245 | return ( 246 | 247 | 248 |
249 | 250 | {installingDependencies ? "Installing dependencies..." : "Finalizing..."} 251 |
252 |
253 |
254 | ); 255 | } 256 | 257 | 258 | return ( 259 | <> 260 | 261 | 262 |
263 |
275 | {hotspotStatus === "running" 276 | ? "Hotspot running" 277 | : hotspotStatus === "stopped" 278 | ? "Hotspot stopped" 279 | : "Processing..."} 280 |
281 | 282 | {hotspotStatus === "running" && ( 283 |
284 | Host IP: {ipAddress} 285 |
286 | )} 287 |
288 | 305 | {hotspotStatus === "loading" ? ( 306 | 307 | ) : hotspotStatus === "running" ? ( 308 | <> 309 | Stop 310 | 311 | ) : ( 312 | <> 313 | Start 314 | 315 | )} 316 | 317 |
318 | {isBlocked && ( 319 | 320 |

⚠ Please enable WiFi to use the hotspot.

321 |
322 | )} 323 |
324 | 325 | {hotspotStatus === "running" && ( 326 | 327 | {Array.isArray(connectedDevices) && connectedDevices.length > 0 ? ( 328 | connectedDevices.map((device, index) => ( 329 | 330 |
331 |
332 | {getSignalIcon(device.signal_strength, 32)} 333 |
334 |
335 |
{device.hostname}
336 |
{device.ip}
337 |
338 | 346 | handleKickDevice(device.mac)} 348 | style={{ 349 | width: "32px", 350 | height: "32px", 351 | padding: 0, 352 | marginRight: "16px", 353 | display: "flex", 354 | alignItems: "center", 355 | justifyContent: "center", 356 | minWidth: "32px", 357 | maxWidth: "32px", 358 | }} 359 | > 360 | 361 | 362 | 363 |
364 |
365 | )) 366 | ) : ( 367 | 368 |

No devices connected.

369 |
370 | )} 371 |
372 | )} 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 385 | showWifiSettingsModal( 386 | ssid, 387 | passphrase, 388 | alwaysUseStoredCredentials, 389 | baseIp, 390 | dhcpStart, 391 | dhcpEnd, 392 | async ( 393 | newSsid: string, 394 | newPassphrase: string, 395 | alwaysUse: boolean, 396 | ip: string, 397 | dhcpStart: string, 398 | dhcpEnd: string 399 | ) => { 400 | const updatedConfig = await callable< 401 | [string, string, boolean], 402 | { ssid: string; passphrase: string; always_use_stored_credentials: boolean } 403 | >("update_credentials")(newSsid, newPassphrase, alwaysUse); 404 | 405 | setSsid(updatedConfig.ssid); 406 | setPassphrase(updatedConfig.passphrase); 407 | setAlwaysUseStoredCredentials(updatedConfig.always_use_stored_credentials); 408 | setBaseIp(ip); 409 | setDhcpStart(dhcpStart); 410 | setDhcpEnd(dhcpEnd); 411 | } 412 | ) 413 | } 414 | > 415 | Edit WiFi Settings 416 | 417 | 418 | 419 | 420 | ); 421 | }; 422 | 423 | 424 | export default definePlugin(() => { 425 | console.log("Hotspot plugin initializing"); 426 | 427 | const onMuonDeviceEvent = async (payload: any) => { 428 | const msg = typeof payload === "string" 429 | ? (() => { try { return JSON.parse(payload); } catch { return {}; } })() 430 | : payload; 431 | 432 | if (msg?.type === "connected") { 433 | toaster.toast({ 434 | title: "Device Connected", 435 | body: `${msg.hostname ?? "Unknown"} (${msg.ip ?? msg.mac ?? "?"})`, 436 | showToast: true, 437 | }); 438 | } else if (msg?.type === "disconnected") { 439 | toaster.toast({ 440 | title: "Device Disconnected", 441 | body: msg.hostname ?? msg.mac ?? "Unknown device", 442 | showToast: true, 443 | }); 444 | } 445 | }; 446 | if (!_muonListenerRegistered) { 447 | addEventListener("muon_device_event", onMuonDeviceEvent); 448 | _muonListenerRegistered = true; 449 | } 450 | 451 | const suspendRequestRegistration = 452 | window.SteamClient.System.RegisterForOnSuspendRequest?.bind(window.SteamClient.System) ?? 453 | sleepManager?.RegisterForNotifyRequestSuspend; 454 | 455 | const suspendResumeRegistration = 456 | window.SteamClient.System.RegisterForOnResumeFromSuspend?.bind(window.SteamClient.System) ?? 457 | sleepManager?.RegisterForNotifyResumeFromSuspend; 458 | 459 | const unregisterSuspend = suspendRequestRegistration 460 | ? suspendRequestRegistration(() => call<[]>("suspend_ap")) 461 | : { unregister: () => {} }; 462 | 463 | const unregisterResume = suspendResumeRegistration 464 | ? suspendResumeRegistration(() => call<[]>("resume_ap")) 465 | : { unregister: () => {} }; 466 | 467 | return { 468 | name: "Muon", 469 | titleView:
Muon
, 470 | content: , 471 | icon: , 472 | onDismount() { 473 | unregisterSuspend.unregister(); 474 | unregisterResume.unregister(); 475 | if (_muonListenerRegistered) { 476 | removeEventListener("muon_device_event", onMuonDeviceEvent); 477 | _muonListenerRegistered = false; 478 | } 479 | } 480 | }; 481 | }); -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import asyncio 4 | import re 5 | import decky 6 | import subprocess 7 | from pathlib import Path 8 | from settings import SettingsManager 9 | 10 | class Plugin: 11 | # Define default WiFi interface, plugin directory, settings file, IP/DHCP range, and initialise statuses. 12 | def __init__(self): 13 | self.debug = False 14 | self.wifi_interface = "wlan0" 15 | self.ap_interface = "muon0" 16 | self.settingsDir = os.environ.get("DECKY_PLUGIN_SETTINGS_DIR", "/tmp") 17 | self.assetsDir = Path(decky.DECKY_PLUGIN_DIR) / "assets" 18 | self.ip_address = "192.168.8.1" 19 | self.dhcp_range = "192.168.8.100,192.168.8.200,12h" 20 | self.hotspot_active = False 21 | self.ssid = None 22 | self.passphrase = None 23 | self.current_directory = os.path.dirname(__file__) 24 | if self.debug: 25 | decky.logger.debug(f"Muon initialised. Settings directory: {self.settingsDir}, Assets directory: {self.assetsDir}") 26 | 27 | async def _main(self): 28 | decky.logger.info("Hotspot Plugin Loaded") 29 | self.settings = SettingsManager(name="hotspot_settings", settings_directory=self.settingsDir) 30 | self.settings.read() 31 | await self.load_settings() 32 | asyncio.create_task(self.monitor_connected_devices()) 33 | 34 | async def _unload(self): 35 | decky.logger.info("Stopping Hotspot Plugin") 36 | if self.hotspot_active: 37 | await self.stop_hotspot() 38 | decky.logger.info("Plugin Unloaded") 39 | 40 | async def _uninstall(self): 41 | decky.logger.info("Stopping Hotspot Plugin") 42 | if self.hotspot_active: 43 | await self.stop_hotspot() 44 | 45 | decky.logger.info("Cleaning up dependencies.") 46 | script_path = os.path.join(self.assetsDir, "remove_dependencies.sh") 47 | await self.run_command( 48 | f"bash {script_path}" 49 | ) 50 | 51 | # SYSEXT METHODS 52 | async def activate_muon_sysext(self): 53 | muon_raw = os.path.join(self.assetsDir, "muon.raw") 54 | link_path = "/var/lib/extensions/muon.raw" 55 | if not os.path.exists(muon_raw): 56 | decky.logger.warning("muon.raw not found - attempting to build via install script.") 57 | await self.run_command(f"bash {os.path.join(self.assetsDir, 'install_dependencies.sh')}") 58 | 59 | # Link into /var/lib/extensions so refresh will include it 60 | await self.run_command(f"sudo ln -sf '{muon_raw}' '{link_path}'") 61 | # Refresh sysext to include Muon 62 | try: 63 | out = await self.run_command("systemd-sysext refresh") 64 | decky.logger.info(f"sysext refresh output: {out}") 65 | except Exception as e: 66 | decky.logger.error(f"sysext refresh after activation failed: {e}") 67 | 68 | async def deactivate_muon_sysext(self): 69 | link_path = "/var/lib/extensions/muon.raw" 70 | if os.path.exists(link_path) or os.path.islink(link_path): 71 | await self.run_command(f"sudo rm -f '{link_path}'") 72 | 73 | # Refresh sysext after removing Muon 74 | try: 75 | out = await self.run_command("systemd-sysext refresh") 76 | decky.logger.info(f"sysext refresh output: {out}") 77 | except Exception as e: 78 | decky.logger.error(f"sysext refresh after deactivation failed: {e}") 79 | 80 | 81 | # SETTINGS METHODS 82 | async def load_settings(self): 83 | # Ensures SSID and passphrase are properly initialized in all cases and returns them to the frontend. 84 | always_use = self.settings.getSetting("always_use_stored_credentials", "false") 85 | self.always_use_stored_credentials = always_use == "true" 86 | 87 | # Check if SSID and passphrase are set. If not, load from settings. 88 | if not (self.ssid and self.passphrase): 89 | stored_ssid = self.settings.getSetting("ssid", None) 90 | stored_passphrase = self.settings.getSetting("passphrase", None) 91 | 92 | if self.always_use_stored_credentials: 93 | # Use stored creds, or use the Steam Deck hostname and generate random password if not set. 94 | if not (stored_ssid and stored_passphrase): 95 | decky.logger.warning("[Settings] Stored credentials missing! Generating failsafe.") 96 | stored_ssid = await self.get_hostname() 97 | stored_passphrase = self.generate_random_password() 98 | await self.update_credentials(stored_ssid, stored_passphrase, True) 99 | 100 | self.ssid = stored_ssid 101 | self.passphrase = stored_passphrase 102 | else: 103 | # Failsafe is to use the Steam Deck hostname and generate random password. 104 | self.ssid = await self.get_hostname() 105 | self.passphrase = self.generate_random_password() 106 | 107 | # Use the IP address and DHCP range from the settings if available, if not use the defaults. 108 | self.ip_address = self.settings.getSetting("ip_address", "192.168.8.1") 109 | self.dhcp_range = self.settings.getSetting("dhcp_range", "192.168.8.100,192.168.8.200,12h") 110 | decky.logger.info(f"[Settings] SSID={self.ssid}, Passphrase={self.passphrase}, AlwaysUseStored={self.always_use_stored_credentials}") 111 | return { 112 | "ssid": self.ssid, 113 | "passphrase": self.passphrase, 114 | "always_use_stored_credentials": self.always_use_stored_credentials, 115 | "ip_address": self.ip_address, 116 | "dhcp_range": self.dhcp_range, 117 | } 118 | 119 | async def settings_read(self): 120 | # Read settings from storage. 121 | decky.logger.info("Reading hotspot settings...") 122 | 123 | ssid = self.settings.getSetting("ssid", None) 124 | passphrase = self.settings.getSetting("passphrase", None) 125 | always_use = self.settings.getSetting("always_use_stored_credentials", "false") 126 | 127 | if not ssid or not passphrase: 128 | await self.load_settings() 129 | ssid = self.settings.getSetting("ssid") 130 | passphrase = self.settings.getSetting("passphrase") 131 | always_use = self.settings.getSetting("always_use_stored_credentials", "false") 132 | 133 | return {"ssid": ssid, "passphrase": passphrase, "always_use_stored_credentials": always_use} 134 | 135 | async def update_credentials(self, new_ssid, new_passphrase, always_use): 136 | # Updates SSID and passphrase, storing only if always_use_stored_credentials is enabled. 137 | self.ssid = new_ssid 138 | self.passphrase = new_passphrase 139 | self.always_use_stored_credentials = always_use 140 | 141 | if always_use: 142 | self.settings.setSetting("ssid", new_ssid) 143 | self.settings.setSetting("passphrase", new_passphrase) 144 | self.settings.setSetting("always_use_stored_credentials", "true") 145 | self.settings.commit() 146 | else: 147 | self.settings.setSetting("always_use_stored_credentials", "false") 148 | self.settings.commit() 149 | 150 | decky.logger.info(f"Updated credentials: SSID={self.ssid}, Passphrase={self.passphrase}, AlwaysUse={self.always_use_stored_credentials}") 151 | 152 | return {"ssid": self.ssid, "passphrase": self.passphrase, "always_use_stored_credentials": self.always_use_stored_credentials} 153 | 154 | # HOTSPOT CONTROL METHODS 155 | async def start_hotspot(self): 156 | decky.logger.info("Starting Hotspot") 157 | 158 | try: 159 | ssid = self.ssid 160 | passphrase = self.passphrase 161 | 162 | decky.logger.info(f"Using SSID: {ssid}, Passphrase: {passphrase} (Always Use: {self.always_use_stored_credentials})") 163 | 164 | if not ssid or not passphrase: 165 | decky.logger.error("SSID or Passphrase is missing! Aborting.") 166 | return False 167 | 168 | await self.activate_muon_sysext() 169 | await self.check_dependencies(temporary_sysext=False) 170 | await self.configure_firewalld() 171 | await self.ensure_wlan0_up() 172 | await self.capture_network_config() 173 | await self.capture_service_states() 174 | await self.start_wifi_ap(ssid, passphrase) 175 | await self.start_dhcp_server() 176 | 177 | decky.logger.info("Hotspot activated.") 178 | self.wlan_ip = await self.run_command(fr"ip addr show {self.ap_interface} | grep -oP 'inet \K[\d.]+/\d+'") 179 | decky.logger.info(f"Using WLAN IP address: {self.wlan_ip}") 180 | 181 | decky.logger.info("Checking if hotspot activated successfully...") 182 | # Failsafe - if the hotspot is not actually running after this, cleanly bring the WiFi back online. 183 | if not await self.is_hotspot_active(): 184 | decky.logger.error("Failsafe triggered: Hotspot did not start correctly.") 185 | await self.stop_hotspot() 186 | self.hotspot_active = False 187 | return False 188 | 189 | self.hotspot_active = True 190 | decky.logger.info("Hotspot started successfully.") 191 | return True 192 | 193 | except Exception as e: 194 | decky.logger.error(f"Failed to start hotspot: {str(e)}") 195 | return False 196 | 197 | async def stop_hotspot(self): 198 | decky.logger.info("Stopping Hotspot") 199 | try: 200 | script_path = os.path.join(self.assetsDir, "stop_hotspot.sh") 201 | dns_servers = ",".join(self.original_dns) if self.original_dns else "" 202 | 203 | decky.logger.info("Restoring network configuration") 204 | 205 | result = await self.run_command( 206 | f"bash {script_path} {self.wifi_interface} {self.original_ip or ''} {self.original_gateway or ''} {dns_servers}" 207 | ) 208 | 209 | if "Network configuration restored successfully" in result: 210 | self.hotspot_active = False 211 | decky.logger.info("Network configuration restored successfully.") 212 | else: 213 | decky.logger.error("Failed to restore network configuration.") 214 | await self.deactivate_muon_sysext() 215 | decky.logger.info("Hotspot stopped") 216 | except Exception as e: 217 | decky.logger.error(f"Failed to stop hotspot: {str(e)}") 218 | 219 | async def is_hotspot_active(self) -> bool: 220 | # Checks if the hostapd service is running. 221 | try: 222 | result = await self.run_command("pgrep -x hostapd", check=False) 223 | is_active = bool(result.strip()) 224 | decky.logger.info(f"Hotspot status: {'Active' if is_active else 'Inactive'}") 225 | return is_active 226 | except Exception as e: 227 | decky.logger.error(f"Error checking hotspot status: {e}") 228 | return False 229 | 230 | async def start_wifi_ap(self, ssid, passphrase): 231 | decky.logger.info("Starting Hotspot") 232 | script_path = os.path.join(self.assetsDir, "start_hotspot.sh") 233 | 234 | result = await self.run_command([ 235 | "bash", 236 | script_path, 237 | self.wifi_interface, 238 | self.ip_address, 239 | ssid, 240 | passphrase 241 | ]) 242 | 243 | if "Hotspot started successfully" in result: 244 | self.hotspot_active = True 245 | decky.logger.info("Hotspot is active.") 246 | else: 247 | decky.logger.error("Failed to start Hotspot.") 248 | 249 | # Check if the WiFi has been disabled 250 | async def is_rfkill_blocking_wlan(self): 251 | try: 252 | rfkill_output = await self.run_command("rfkill list") 253 | 254 | if not rfkill_output: 255 | decky.logger.error("rfkill command returned empty output.") 256 | return False # Default to not blocked 257 | 258 | in_wlan_section = False 259 | for line in rfkill_output.splitlines(): 260 | line = line.strip() 261 | if "Wireless LAN" in line: 262 | in_wlan_section = True 263 | elif in_wlan_section: 264 | if "Soft blocked: yes" in line or "Hard blocked: yes" in line: 265 | return True # WLAN is blocked 266 | if line.startswith("0:") or line.startswith("1:"): # Next device section 267 | break # Stop checking after Wireless LAN section 268 | 269 | return False # Not blocked 270 | 271 | except Exception as e: 272 | decky.logger.error(f"Error checking rfkill: {e}") 273 | return False # Default to not blocked if there's an error 274 | 275 | # DEPENDENCY MANAGEMENT METHODS 276 | async def check_dependencies(self, temporary_sysext=True): 277 | # Ensure required dependencies are installed. 278 | try: 279 | if self.hotspot_active: 280 | temporary_sysext = False 281 | 282 | if temporary_sysext: 283 | await self.activate_muon_sysext() 284 | 285 | statuses = {} 286 | for dep in ["dnsmasq", "hostapd"]: 287 | result = await self.run_command(f"which {dep}") 288 | statuses[dep] = bool(result) 289 | if not result: 290 | decky.logger.error(f"ERROR: `{dep}` is not installed.") 291 | decky.logger.info("Dependency statuses: " + str(statuses)) 292 | return statuses 293 | finally: 294 | # Deactivate Muon sysext after checking dependencies 295 | try: 296 | if temporary_sysext and not self.hostpot_active: 297 | await self.deactivate_muon_sysext() 298 | decky.logger.info("Muon sysext deactivated after dependency check.") 299 | except Exception as e: 300 | decky.logger.error(f"Failed to deactivate Muon sysext after check: {e}") 301 | 302 | async def install_dependencies(self): 303 | # Path to install script 304 | script_path = os.path.join(self.assetsDir, "install_dependencies.sh") 305 | 306 | decky.logger.info(f"Installing dependencies via shell script.") 307 | 308 | result = await self.run_command( 309 | f"bash {script_path}", 310 | cwd=self.assetsDir 311 | ) 312 | 313 | # Recheck dependencies after script runs 314 | statuses = await self.check_dependencies() 315 | 316 | missing = [dep for dep, ok in statuses.items() if not ok] 317 | if missing: 318 | decky.logger.error(f"Missing after install: {missing}") 319 | return {"success": False, "missing": missing} 320 | 321 | decky.logger.info("All dependencies confirmed installed.") 322 | return {"success": True} 323 | 324 | # NETWORK CONFIGURATION AND SERVICE METHODS 325 | async def capture_network_config(self): 326 | script_path = os.path.join(self.assetsDir, "extract_network_config.sh") 327 | decky.logger.info("Extracting network configuration via Shell Script") 328 | 329 | result = await self.run_command(f"bash {script_path} {self.wifi_interface}") 330 | 331 | # Parsing of shell output into a dictionary 332 | config = {} 333 | for line in result.splitlines(): 334 | if "=" in line: 335 | key, value = line.split("=", 1) 336 | config[key.strip()] = value.strip() 337 | 338 | ip_address = config.get("IP_ADDRESS") 339 | gateway = config.get("GATEWAY") 340 | dns_servers = config.get("DNS_SERVERS", "").split(",") 341 | 342 | decky.logger.info(f"Extracted IP: {ip_address}") 343 | decky.logger.info(f"Extracted Gateway: {gateway}") 344 | decky.logger.info(f"Extracted DNS Servers: {dns_servers}") 345 | 346 | self.original_ip = ip_address 347 | self.original_gateway = gateway 348 | self.original_dns = dns_servers 349 | decky.logger.info(f"Captured original network config: IP={self.original_ip}, Gateway={self.original_gateway}, DNS={self.original_dns}") 350 | 351 | return ip_address, gateway, dns_servers 352 | 353 | async def capture_service_states(self): 354 | # Capture the current state of NetworkManager and iwd before stopping them. 355 | decky.logger.info("Capturing service states for NetworkManager and iwd...") 356 | # Initialise variable for storing service states 357 | self.service_states = {} 358 | # Array of services to check 359 | services = ["NetworkManager", "iwd"] 360 | 361 | # For each service in the services array: 362 | for service in services: 363 | # Check if the service is running 364 | status = await self.run_command(f"sudo systemctl is-active {service}") 365 | # Save the state of the service into the service_states array 366 | self.service_states[service] = status.strip() == "active" # Store True if active, False if inactive 367 | decky.logger.info(f"Service {service}: {'Active' if self.service_states[service] else 'Inactive'}") 368 | 369 | decky.logger.info(f"Captured service states: {self.service_states}") 370 | 371 | async def configure_firewalld(self): 372 | # Configure firewalld for broadcast and DHCP traffic using a shell script. 373 | script_path = os.path.join(self.assetsDir, "change_firewall_settings.sh") 374 | 375 | decky.logger.info("Configuring firewalld...") 376 | 377 | result = await self.run_command( 378 | f"bash {script_path} {self.ip_address}" 379 | ) 380 | 381 | if "Firewalld configuration updated successfully" in result: 382 | decky.logger.info("Firewalld configured successfully.") 383 | else: 384 | decky.logger.error("Failed to configure firewalld.") 385 | 386 | async def update_dhcp(self, ip_address: str, dhcp_start: str, dhcp_end: str, lease_time: str = "12h"): 387 | import ipaddress 388 | try: 389 | ip = ipaddress.IPv4Address(ip_address) 390 | start = ipaddress.IPv4Address(dhcp_start) 391 | end = ipaddress.IPv4Address(dhcp_end) 392 | 393 | for addr in [ip, start, end]: 394 | if not addr.is_private: 395 | raise ValueError("Address not in private range.") 396 | 397 | if ip.exploded.rsplit('.', 1)[0] != start.exploded.rsplit('.', 1)[0] or start.exploded.rsplit('.', 1)[0] != end.exploded.rsplit('.', 1)[0]: 398 | raise ValueError("All addresses must be in the same /24 subnet.") 399 | 400 | if int(start) >= int(end): 401 | raise ValueError("DHCP start must be less than end.") 402 | 403 | self.ip_address = str(ip) 404 | self.dhcp_range = f"{start},{end},{lease_time}" 405 | 406 | self.settings.setSetting("ip_address", self.ip_address) 407 | self.settings.setSetting("dhcp_range", self.dhcp_range) 408 | self.settings.commit() 409 | 410 | return {"ip_address": self.ip_address, "dhcp_range": self.dhcp_range} 411 | except Exception as e: 412 | decky.logger.error(f"Failed to update DHCP config: {e}") 413 | return {"error": str(e)} 414 | 415 | async def start_dhcp_server(self): 416 | # Start the DHCP server using a shell script. 417 | script_path = os.path.join(self.assetsDir, "start_dhcp_server.sh") 418 | 419 | decky.logger.info("Starting DHCP Server.") 420 | 421 | result = await self.run_command( 422 | f"bash {script_path} {self.ap_interface} {self.dhcp_range} {self.ip_address}" 423 | ) 424 | 425 | if "dnsmasq is running" in result: 426 | decky.logger.info("DHCP Server started successfully.") 427 | else: 428 | decky.logger.error("Failed to start DHCP Server.") 429 | 430 | async def get_ip_address(self) -> str: 431 | return self.ip_address 432 | 433 | 434 | 435 | # SUSPENSION METHODS 436 | async def suspend_ap(self): 437 | # This function disables the hotspot if the Steam Deck is suspended. 438 | if self.hotspot_active: 439 | decky.logger.info("Suspending, disabling hotspot...") 440 | await self.stop_hotspot() 441 | 442 | async def resume_ap(self): 443 | # Function for resuming the hotspot after suspension. At the moment, only a log entry. 444 | decky.logger.info("Resuming from suspension...") 445 | 446 | 447 | # CLIENT LIST METHODS 448 | 449 | async def monitor_connected_devices(self): 450 | known = set() 451 | while True: 452 | try: 453 | if await self.is_hotspot_active(): 454 | raw = await self.get_connected_devices() 455 | try: 456 | devices = json.loads(raw) if isinstance(raw, str) else raw 457 | except Exception: 458 | devices = [] 459 | 460 | if not devices: 461 | continue 462 | 463 | if not isinstance(devices, list): 464 | decky.logger.warning(f"[Muon] Invalid device list: {devices}") 465 | await asyncio.sleep(2) 466 | continue 467 | 468 | macs = {d.get("mac") for d in devices if d.get("mac")} 469 | 470 | for d in devices: 471 | mac = d.get("mac") 472 | if mac and mac not in known: 473 | await decky.emit("muon_device_event", { 474 | "type": "connected", 475 | "hostname": d.get("hostname"), 476 | "ip": d.get("ip"), 477 | "mac": mac, 478 | }) 479 | known.add(mac) 480 | 481 | for mac in list(known): 482 | if mac not in macs: 483 | await decky.emit("muon_device_event", { 484 | "type": "disconnected", 485 | "mac": mac, 486 | }) 487 | known.remove(mac) 488 | except Exception as e: 489 | decky.logger.error(f"[Error] {e}") 490 | await asyncio.sleep(2) 491 | 492 | async def get_connected_devices(self): 493 | # Combines output from hostapd_cli and dnsmasq to return connected devices info 494 | # in JSON format. 495 | decky.logger.info("Fetching connected devices...") 496 | 497 | # Hostapd and dnsmasq locations 498 | hostapd_cmd = f"sudo hostapd_cli -p /var/run/hostapd -i {self.ap_interface} all_sta" 499 | dnsmasq_leases_file = "/var/lib/misc/dnsmasq.leases" 500 | 501 | # Dictionary to store device info 502 | devices = {} 503 | 504 | try: 505 | # Get the output from hostapd_cli using the run_command utility 506 | hostapd_output = await self.run_command(hostapd_cmd) 507 | 508 | # Parse hostapd output for MAC and signal strength 509 | current_mac = None 510 | for line in hostapd_output.splitlines(): 511 | line = line.strip() 512 | 513 | # Match MAC address 514 | if re.match(r"^[0-9a-fA-F:]{17}$", line): 515 | current_mac = line 516 | devices[current_mac] = { 517 | "mac": current_mac, 518 | "ip": None, 519 | "hostname": None, 520 | "signal_strength": None 521 | } 522 | # Match signal strength 523 | elif line.startswith("signal=") and current_mac: 524 | signal_strength = int(line.split("=")[-1]) 525 | # Normalize to negative 526 | if signal_strength > 0: 527 | signal_strength = -signal_strength 528 | devices[current_mac]["signal_strength"] = signal_strength 529 | 530 | except Exception as e: 531 | decky.logger.error(f"Error running hostapd_cli: {str(e)}") 532 | return json.dumps({"error": "Failed to retrieve data from hostapd_cli"}) 533 | 534 | # Read and parse dnsmasq leases file 535 | try: 536 | with open(dnsmasq_leases_file, 'r') as f: 537 | for line in f: 538 | parts = line.strip().split() 539 | if len(parts) >= 4: 540 | mac = parts[1] 541 | ip = parts[2] 542 | hostname = parts[3] 543 | 544 | # If MAC is already in devices dict, add IP and hostname 545 | if mac in devices: 546 | devices[mac]["ip"] = ip 547 | devices[mac]["hostname"] = hostname 548 | 549 | except FileNotFoundError: 550 | decky.logger.error(f"Error: {dnsmasq_leases_file} not found.") 551 | return json.dumps({"error": "dnsmasq leases file not found"}) 552 | 553 | # Convert to JSON format 554 | connected_devices_json = json.dumps(list(devices.values()), indent=4) 555 | decky.logger.info(f"Connected Devices: {connected_devices_json}") 556 | 557 | return connected_devices_json 558 | 559 | # CLIENT BLACKLISTING METHODS 560 | async def kick_mac(self, mac_address: str) -> bool: 561 | """Kick and block a MAC address from the hotspot.""" 562 | try: 563 | # Deauthenticate the device (kick it off the hotspot) 564 | result = await self.run_command(f"hostapd_cli -i {self.ap_interface} deauthenticate {mac_address}") 565 | 566 | if not result or "OK" not in result: 567 | decky.logger.error(f"Failed to kick MAC address: {mac_address}. Response: {result}") 568 | return False 569 | 570 | decky.logger.info(f"Successfully kicked MAC address: {mac_address}") 571 | 572 | # Add MAC to deny list in hostapd.deny 573 | hostapd_conf = "/etc/hostapd/hostapd.deny" 574 | with open(hostapd_conf, "a") as f: 575 | f.write(f"\n{mac_address}\n") 576 | 577 | decky.logger.info(f"Added {mac_address} to deny list in {hostapd_conf}") 578 | 579 | # Reload hostapd configuration 580 | reload_result = await self.run_command(f"hostapd_cli -p /var/run/hostapd -i {self.ap_interface} reload") 581 | 582 | if reload_result.strip() == "": # Success if output is empty 583 | decky.logger.info("Reloaded hostapd configuration successfully.") 584 | return True 585 | else: 586 | decky.logger.error(f"Failed to reload hostapd configuration. Output: {reload_result}") 587 | return False 588 | 589 | except Exception as e: 590 | decky.logger.error(f"Error while processing MAC address {mac_address}: {e}") 591 | return False 592 | 593 | async def retrieve_ban_list(self) -> list: 594 | # Regex which matches valid MAC addresses. 595 | VALID_MAC_REGEX = re.compile(r"^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$") 596 | # Default MAC addresses included in the hostapd.deny file. We don't need to worry about these. 597 | EXCLUDED_MACS = {"00:20:30:40:50:60", "00:ab:cd:ef:12:34", "00:00:30:40:50:60"} 598 | """Retrieves the list of banned MAC addresses from hostapd.deny, filtering out invalid and excluded ones.""" 599 | deny_file = "/etc/hostapd/hostapd.deny" 600 | 601 | try: 602 | if not os.path.exists(deny_file): 603 | decky.logger.warning("Ban list file does not exist. Returning empty list.") 604 | return [] 605 | 606 | with open(deny_file, "r") as f: 607 | mac_addresses = [ 608 | line.strip() 609 | for line in f 610 | if VALID_MAC_REGEX.match(line.strip()) and line.strip() not in EXCLUDED_MACS 611 | ] 612 | 613 | decky.logger.info(f"Retrieved {len(mac_addresses)} valid banned MAC addresses.") 614 | # Return all valid MAC addresses, excluding the default ones. 615 | return mac_addresses 616 | 617 | except Exception as e: 618 | decky.logger.error(f"Error retrieving banned MAC addresses: {e}") 619 | return [] 620 | 621 | async def unban_mac_address(self, mac_address: str) -> bool: 622 | # Removes a MAC address from hostapd.deny and reloads hostapd. 623 | deny_file = "/etc/hostapd/hostapd.deny" 624 | 625 | try: 626 | if not os.path.exists(deny_file): 627 | decky.logger.warning("Ban list file does not exist. Nothing to unban.") 628 | return False 629 | 630 | # Read the file and filter out the MAC address 631 | with open(deny_file, "r") as f: 632 | lines = f.readlines() 633 | 634 | new_lines = [line for line in lines if line.strip().lower() != mac_address.lower()] 635 | 636 | if len(new_lines) == len(lines): # No changes means MAC wasn't found 637 | decky.logger.warning(f"MAC address {mac_address} not found in ban list.") 638 | return False 639 | 640 | # Write back the updated list 641 | with open(deny_file, "w") as f: 642 | f.writelines(new_lines) 643 | 644 | decky.logger.info(f"Unbanned MAC address: {mac_address}") 645 | 646 | # Reload hostapd to apply changes 647 | reload_result = await self.run_command("sudo systemctl reload hostapd") 648 | 649 | if reload_result.strip() == "": # Success if no output 650 | decky.logger.info("Reloaded hostapd configuration successfully.") 651 | return True 652 | else: 653 | decky.logger.error(f"Failed to reload hostapd. Output: {reload_result}") 654 | return False 655 | 656 | except Exception as e: 657 | decky.logger.error(f"Error unbanning MAC address {mac_address}: {e}") 658 | return False 659 | 660 | 661 | # UTILITY METHODS 662 | async def run_command(self, command, check=False, cwd=None): 663 | # Function to run a shell command. 664 | env = os.environ.copy() 665 | env["LD_LIBRARY_PATH"] = "/usr/lib:/usr/lib64:" + env.get("LD_LIBRARY_PATH", "") 666 | 667 | if cwd is None: 668 | cwd = os.path.dirname(__file__) 669 | 670 | if isinstance(command, list): 671 | result = await asyncio.create_subprocess_exec( 672 | *command, 673 | stdout=subprocess.PIPE, 674 | stderr=subprocess.PIPE, 675 | env=env, 676 | cwd=cwd 677 | ) 678 | else: 679 | result = await asyncio.create_subprocess_exec( 680 | "/usr/bin/env", "bash", "-c", command, 681 | stdout=subprocess.PIPE, 682 | stderr=subprocess.PIPE, 683 | env=env, 684 | cwd=cwd 685 | ) 686 | 687 | stdout, stderr = await result.communicate() 688 | if self.debug: 689 | if stdout: 690 | decky.logger.debug(f"Command output: {stdout.decode().strip()}") 691 | if stderr: 692 | decky.logger.error(f"Command error: {stderr.decode().strip()}") 693 | return stdout.decode().strip() 694 | 695 | async def ensure_wlan0_up(self): 696 | # Ensure the wlan0 interface is available and up. 697 | decky.logger.info("Checking wlan0 status...") 698 | # Check the status of the primary wireless networking device (almost always wlan0) 699 | result = await self.run_command("ip link show wlan0") 700 | decky.logger.info(f"wlan0 status: {result}") 701 | 702 | # If the WiFi is down, bring it up: 703 | if "state DOWN" in result: 704 | decky.logger.info("wlan0 is down. Bringing it up...") 705 | await self.run_command("sudo ip link set wlan0 up") 706 | 707 | # If the WiFi chip is missing for some reason (this should never happen, but good to handle it cleanly): 708 | elif "state UNKNOWN" in result: 709 | decky.logger.error("wlan0 interface not found. Check your WiFi adapter.") 710 | raise Exception("wlan0 interface not found.") 711 | 712 | async def get_hostname(self): 713 | # Returns the current system hostname 714 | decky.logger.info("Fetching system hostname...") 715 | return os.uname()[1] 716 | 717 | def generate_random_password(self): 718 | import random 719 | # Randomly select eight characters from the charset variable and return them. Letters and numbers have been chosen to be unambiguous 720 | charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789" 721 | return ''.join(random.choice(charset) for _ in range(8)) 722 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | BSD 3-Clause License 677 | 678 | Copyright (c) 2025, Jenomorph/WTLNetwork 679 | Original Copyright (c) 2025, Steam Deck Homebrew 680 | 681 | All rights reserved. 682 | 683 | Redistribution and use in source and binary forms, with or without 684 | modification, are permitted provided that the following conditions are met: 685 | 686 | 1. Redistributions of source code must retain the above copyright notice, this 687 | list of conditions and the following disclaimer. 688 | 689 | 2. Redistributions in binary form must reproduce the above copyright notice, 690 | this list of conditions and the following disclaimer in the documentation 691 | and/or other materials provided with the distribution. 692 | 693 | 3. Neither the name of the copyright holder nor the names of its 694 | contributors may be used to endorse or promote products derived from 695 | this software without specific prior written permission. 696 | 697 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 698 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 699 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 700 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 701 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 702 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 703 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 704 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 705 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 706 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@decky/api': 12 | specifier: ^1.1.3 13 | version: 1.1.3 14 | react-icons: 15 | specifier: ^5.3.0 16 | version: 5.4.0(react@19.0.0) 17 | tslib: 18 | specifier: ^2.7.0 19 | version: 2.8.1 20 | devDependencies: 21 | '@decky/rollup': 22 | specifier: ^1.0.2 23 | version: 1.0.2 24 | '@decky/ui': 25 | specifier: ^4.11.0 26 | version: 4.11.0 27 | '@types/react': 28 | specifier: 18.3.3 29 | version: 18.3.3 30 | '@types/react-dom': 31 | specifier: 18.3.0 32 | version: 18.3.0 33 | '@types/webpack': 34 | specifier: ^5.28.5 35 | version: 5.28.5 36 | rollup: 37 | specifier: ^4.22.5 38 | version: 4.34.6 39 | typescript: 40 | specifier: ^5.6.2 41 | version: 5.7.3 42 | 43 | packages: 44 | 45 | '@decky/api@1.1.3': 46 | resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==} 47 | 48 | '@decky/rollup@1.0.2': 49 | resolution: {integrity: sha512-ixuHH3msw156ACbgWZi4hgccdzDBIuqYGOVja8sLX4lHFgaWUKji1vomDi7Dk1CamgjmKt+Dqf75Pezv2FB6Bw==} 50 | 51 | '@decky/ui@4.11.0': 52 | resolution: {integrity: sha512-l9PstFC+S8FE8M2kIM78L8cYW4vzJ/ZD30II0huarHLcCsKM4Q+rbmEnbWjlJ1/KLmGXVRXBdAbyD4X/FzfxnQ==} 53 | 54 | '@isaacs/cliui@8.0.2': 55 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 56 | engines: {node: '>=12'} 57 | 58 | '@jridgewell/gen-mapping@0.3.8': 59 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 60 | engines: {node: '>=6.0.0'} 61 | 62 | '@jridgewell/resolve-uri@3.1.2': 63 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 64 | engines: {node: '>=6.0.0'} 65 | 66 | '@jridgewell/set-array@1.2.1': 67 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 68 | engines: {node: '>=6.0.0'} 69 | 70 | '@jridgewell/source-map@0.3.6': 71 | resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} 72 | 73 | '@jridgewell/sourcemap-codec@1.5.0': 74 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 75 | 76 | '@jridgewell/trace-mapping@0.3.25': 77 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 78 | 79 | '@nodelib/fs.scandir@2.1.5': 80 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 81 | engines: {node: '>= 8'} 82 | 83 | '@nodelib/fs.stat@2.0.5': 84 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 85 | engines: {node: '>= 8'} 86 | 87 | '@nodelib/fs.walk@1.2.8': 88 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 89 | engines: {node: '>= 8'} 90 | 91 | '@pkgjs/parseargs@0.11.0': 92 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 93 | engines: {node: '>=14'} 94 | 95 | '@rollup/plugin-commonjs@26.0.3': 96 | resolution: {integrity: sha512-2BJcolt43MY+y5Tz47djHkodCC3c1VKVrBDKpVqHKpQ9z9S158kCCqB8NF6/gzxLdNlYW9abB3Ibh+kOWLp8KQ==} 97 | engines: {node: '>=16.0.0 || 14 >= 14.17'} 98 | peerDependencies: 99 | rollup: ^2.68.0||^3.0.0||^4.0.0 100 | peerDependenciesMeta: 101 | rollup: 102 | optional: true 103 | 104 | '@rollup/plugin-json@6.1.0': 105 | resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} 106 | engines: {node: '>=14.0.0'} 107 | peerDependencies: 108 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 109 | peerDependenciesMeta: 110 | rollup: 111 | optional: true 112 | 113 | '@rollup/plugin-node-resolve@15.3.1': 114 | resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} 115 | engines: {node: '>=14.0.0'} 116 | peerDependencies: 117 | rollup: ^2.78.0||^3.0.0||^4.0.0 118 | peerDependenciesMeta: 119 | rollup: 120 | optional: true 121 | 122 | '@rollup/plugin-replace@5.0.7': 123 | resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} 124 | engines: {node: '>=14.0.0'} 125 | peerDependencies: 126 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 127 | peerDependenciesMeta: 128 | rollup: 129 | optional: true 130 | 131 | '@rollup/plugin-typescript@11.1.6': 132 | resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} 133 | engines: {node: '>=14.0.0'} 134 | peerDependencies: 135 | rollup: ^2.14.0||^3.0.0||^4.0.0 136 | tslib: '*' 137 | typescript: '>=3.7.0' 138 | peerDependenciesMeta: 139 | rollup: 140 | optional: true 141 | tslib: 142 | optional: true 143 | 144 | '@rollup/pluginutils@5.1.4': 145 | resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} 146 | engines: {node: '>=14.0.0'} 147 | peerDependencies: 148 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 149 | peerDependenciesMeta: 150 | rollup: 151 | optional: true 152 | 153 | '@rollup/rollup-android-arm-eabi@4.34.6': 154 | resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} 155 | cpu: [arm] 156 | os: [android] 157 | 158 | '@rollup/rollup-android-arm64@4.34.6': 159 | resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} 160 | cpu: [arm64] 161 | os: [android] 162 | 163 | '@rollup/rollup-darwin-arm64@4.34.6': 164 | resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} 165 | cpu: [arm64] 166 | os: [darwin] 167 | 168 | '@rollup/rollup-darwin-x64@4.34.6': 169 | resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} 170 | cpu: [x64] 171 | os: [darwin] 172 | 173 | '@rollup/rollup-freebsd-arm64@4.34.6': 174 | resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} 175 | cpu: [arm64] 176 | os: [freebsd] 177 | 178 | '@rollup/rollup-freebsd-x64@4.34.6': 179 | resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} 180 | cpu: [x64] 181 | os: [freebsd] 182 | 183 | '@rollup/rollup-linux-arm-gnueabihf@4.34.6': 184 | resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} 185 | cpu: [arm] 186 | os: [linux] 187 | 188 | '@rollup/rollup-linux-arm-musleabihf@4.34.6': 189 | resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} 190 | cpu: [arm] 191 | os: [linux] 192 | 193 | '@rollup/rollup-linux-arm64-gnu@4.34.6': 194 | resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} 195 | cpu: [arm64] 196 | os: [linux] 197 | 198 | '@rollup/rollup-linux-arm64-musl@4.34.6': 199 | resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} 200 | cpu: [arm64] 201 | os: [linux] 202 | 203 | '@rollup/rollup-linux-loongarch64-gnu@4.34.6': 204 | resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} 205 | cpu: [loong64] 206 | os: [linux] 207 | 208 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': 209 | resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} 210 | cpu: [ppc64] 211 | os: [linux] 212 | 213 | '@rollup/rollup-linux-riscv64-gnu@4.34.6': 214 | resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} 215 | cpu: [riscv64] 216 | os: [linux] 217 | 218 | '@rollup/rollup-linux-s390x-gnu@4.34.6': 219 | resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} 220 | cpu: [s390x] 221 | os: [linux] 222 | 223 | '@rollup/rollup-linux-x64-gnu@4.34.6': 224 | resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} 225 | cpu: [x64] 226 | os: [linux] 227 | 228 | '@rollup/rollup-linux-x64-musl@4.34.6': 229 | resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} 230 | cpu: [x64] 231 | os: [linux] 232 | 233 | '@rollup/rollup-win32-arm64-msvc@4.34.6': 234 | resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} 235 | cpu: [arm64] 236 | os: [win32] 237 | 238 | '@rollup/rollup-win32-ia32-msvc@4.34.6': 239 | resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} 240 | cpu: [ia32] 241 | os: [win32] 242 | 243 | '@rollup/rollup-win32-x64-msvc@4.34.6': 244 | resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} 245 | cpu: [x64] 246 | os: [win32] 247 | 248 | '@types/eslint-scope@3.7.7': 249 | resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} 250 | 251 | '@types/eslint@9.6.1': 252 | resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} 253 | 254 | '@types/estree@1.0.6': 255 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 256 | 257 | '@types/glob@7.2.0': 258 | resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} 259 | 260 | '@types/json-schema@7.0.15': 261 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 262 | 263 | '@types/minimatch@5.1.2': 264 | resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} 265 | 266 | '@types/node@22.13.1': 267 | resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} 268 | 269 | '@types/prop-types@15.7.14': 270 | resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} 271 | 272 | '@types/react-dom@18.3.0': 273 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 274 | 275 | '@types/react@18.3.3': 276 | resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} 277 | 278 | '@types/resolve@1.20.2': 279 | resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 280 | 281 | '@types/webpack@5.28.5': 282 | resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} 283 | 284 | '@webassemblyjs/ast@1.14.1': 285 | resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} 286 | 287 | '@webassemblyjs/floating-point-hex-parser@1.13.2': 288 | resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} 289 | 290 | '@webassemblyjs/helper-api-error@1.13.2': 291 | resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} 292 | 293 | '@webassemblyjs/helper-buffer@1.14.1': 294 | resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} 295 | 296 | '@webassemblyjs/helper-numbers@1.13.2': 297 | resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} 298 | 299 | '@webassemblyjs/helper-wasm-bytecode@1.13.2': 300 | resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} 301 | 302 | '@webassemblyjs/helper-wasm-section@1.14.1': 303 | resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} 304 | 305 | '@webassemblyjs/ieee754@1.13.2': 306 | resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} 307 | 308 | '@webassemblyjs/leb128@1.13.2': 309 | resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} 310 | 311 | '@webassemblyjs/utf8@1.13.2': 312 | resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} 313 | 314 | '@webassemblyjs/wasm-edit@1.14.1': 315 | resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} 316 | 317 | '@webassemblyjs/wasm-gen@1.14.1': 318 | resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} 319 | 320 | '@webassemblyjs/wasm-opt@1.14.1': 321 | resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} 322 | 323 | '@webassemblyjs/wasm-parser@1.14.1': 324 | resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} 325 | 326 | '@webassemblyjs/wast-printer@1.14.1': 327 | resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} 328 | 329 | '@xtuc/ieee754@1.2.0': 330 | resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 331 | 332 | '@xtuc/long@4.2.2': 333 | resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 334 | 335 | acorn@8.14.0: 336 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 337 | engines: {node: '>=0.4.0'} 338 | hasBin: true 339 | 340 | aggregate-error@3.1.0: 341 | resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} 342 | engines: {node: '>=8'} 343 | 344 | ajv-formats@2.1.1: 345 | resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} 346 | peerDependencies: 347 | ajv: ^8.0.0 348 | peerDependenciesMeta: 349 | ajv: 350 | optional: true 351 | 352 | ajv-keywords@3.5.2: 353 | resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} 354 | peerDependencies: 355 | ajv: ^6.9.1 356 | 357 | ajv-keywords@5.1.0: 358 | resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} 359 | peerDependencies: 360 | ajv: ^8.8.2 361 | 362 | ajv@6.12.6: 363 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 364 | 365 | ajv@8.17.1: 366 | resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} 367 | 368 | ansi-regex@5.0.1: 369 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 370 | engines: {node: '>=8'} 371 | 372 | ansi-regex@6.1.0: 373 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 374 | engines: {node: '>=12'} 375 | 376 | ansi-styles@4.3.0: 377 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 378 | engines: {node: '>=8'} 379 | 380 | ansi-styles@6.2.1: 381 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 382 | engines: {node: '>=12'} 383 | 384 | array-union@2.1.0: 385 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 386 | engines: {node: '>=8'} 387 | 388 | balanced-match@1.0.2: 389 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 390 | 391 | brace-expansion@1.1.11: 392 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 393 | 394 | brace-expansion@2.0.1: 395 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 396 | 397 | braces@3.0.3: 398 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 399 | engines: {node: '>=8'} 400 | 401 | browserslist@4.24.4: 402 | resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} 403 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 404 | hasBin: true 405 | 406 | buffer-from@1.1.2: 407 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 408 | 409 | caniuse-lite@1.0.30001699: 410 | resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} 411 | 412 | chrome-trace-event@1.0.4: 413 | resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} 414 | engines: {node: '>=6.0'} 415 | 416 | clean-stack@2.2.0: 417 | resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} 418 | engines: {node: '>=6'} 419 | 420 | color-convert@2.0.1: 421 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 422 | engines: {node: '>=7.0.0'} 423 | 424 | color-name@1.1.4: 425 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 426 | 427 | commander@2.20.3: 428 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 429 | 430 | commondir@1.0.1: 431 | resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} 432 | 433 | concat-map@0.0.1: 434 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 435 | 436 | cross-spawn@7.0.6: 437 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 438 | engines: {node: '>= 8'} 439 | 440 | csstype@3.1.3: 441 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 442 | 443 | deepmerge@4.3.1: 444 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 445 | engines: {node: '>=0.10.0'} 446 | 447 | del@5.1.0: 448 | resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} 449 | engines: {node: '>=8'} 450 | 451 | dir-glob@3.0.1: 452 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 453 | engines: {node: '>=8'} 454 | 455 | eastasianwidth@0.2.0: 456 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 457 | 458 | electron-to-chromium@1.5.96: 459 | resolution: {integrity: sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==} 460 | 461 | emoji-regex@8.0.0: 462 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 463 | 464 | emoji-regex@9.2.2: 465 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 466 | 467 | enhanced-resolve@5.18.1: 468 | resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} 469 | engines: {node: '>=10.13.0'} 470 | 471 | es-module-lexer@1.6.0: 472 | resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} 473 | 474 | escalade@3.2.0: 475 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 476 | engines: {node: '>=6'} 477 | 478 | eslint-scope@5.1.1: 479 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 480 | engines: {node: '>=8.0.0'} 481 | 482 | esrecurse@4.3.0: 483 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 484 | engines: {node: '>=4.0'} 485 | 486 | estraverse@4.3.0: 487 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 488 | engines: {node: '>=4.0'} 489 | 490 | estraverse@5.3.0: 491 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 492 | engines: {node: '>=4.0'} 493 | 494 | estree-walker@0.6.1: 495 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 496 | 497 | estree-walker@2.0.2: 498 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 499 | 500 | estree-walker@3.0.3: 501 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 502 | 503 | events@3.3.0: 504 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 505 | engines: {node: '>=0.8.x'} 506 | 507 | fast-deep-equal@3.1.3: 508 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 509 | 510 | fast-glob@3.3.3: 511 | resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 512 | engines: {node: '>=8.6.0'} 513 | 514 | fast-json-stable-stringify@2.1.0: 515 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 516 | 517 | fast-uri@3.0.6: 518 | resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} 519 | 520 | fastq@1.19.0: 521 | resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} 522 | 523 | fill-range@7.1.1: 524 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 525 | engines: {node: '>=8'} 526 | 527 | foreground-child@3.3.0: 528 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 529 | engines: {node: '>=14'} 530 | 531 | fs.realpath@1.0.0: 532 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 533 | 534 | fsevents@2.3.3: 535 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 536 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 537 | os: [darwin] 538 | 539 | function-bind@1.1.2: 540 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 541 | 542 | glob-parent@5.1.2: 543 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 544 | engines: {node: '>= 6'} 545 | 546 | glob-to-regexp@0.4.1: 547 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 548 | 549 | glob@10.4.5: 550 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 551 | hasBin: true 552 | 553 | glob@7.2.3: 554 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 555 | deprecated: Glob versions prior to v9 are no longer supported 556 | 557 | globby@10.0.2: 558 | resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} 559 | engines: {node: '>=8'} 560 | 561 | graceful-fs@4.2.11: 562 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 563 | 564 | has-flag@4.0.0: 565 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 566 | engines: {node: '>=8'} 567 | 568 | hasown@2.0.2: 569 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 570 | engines: {node: '>= 0.4'} 571 | 572 | ignore@5.3.2: 573 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 574 | engines: {node: '>= 4'} 575 | 576 | indent-string@4.0.0: 577 | resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} 578 | engines: {node: '>=8'} 579 | 580 | inflight@1.0.6: 581 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 582 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 583 | 584 | inherits@2.0.4: 585 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 586 | 587 | is-core-module@2.16.1: 588 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 589 | engines: {node: '>= 0.4'} 590 | 591 | is-extglob@2.1.1: 592 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 593 | engines: {node: '>=0.10.0'} 594 | 595 | is-fullwidth-code-point@3.0.0: 596 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 597 | engines: {node: '>=8'} 598 | 599 | is-glob@4.0.3: 600 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 601 | engines: {node: '>=0.10.0'} 602 | 603 | is-module@1.0.0: 604 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 605 | 606 | is-number@7.0.0: 607 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 608 | engines: {node: '>=0.12.0'} 609 | 610 | is-path-cwd@2.2.0: 611 | resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} 612 | engines: {node: '>=6'} 613 | 614 | is-path-inside@3.0.3: 615 | resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 616 | engines: {node: '>=8'} 617 | 618 | is-reference@1.2.1: 619 | resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} 620 | 621 | is-reference@3.0.3: 622 | resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} 623 | 624 | is-what@5.0.2: 625 | resolution: {integrity: sha512-vI7Ui0qzNQ2ClDZd0bC7uqRk3T1imbX5cZODmVlqqdqiwmSIUX3CNSiRgFjFMJ987sVCMSa7xZeEDtpJduPg4A==} 626 | engines: {node: '>=18'} 627 | 628 | isexe@2.0.0: 629 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 630 | 631 | jackspeak@3.4.3: 632 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 633 | 634 | jest-worker@27.5.1: 635 | resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 636 | engines: {node: '>= 10.13.0'} 637 | 638 | json-parse-even-better-errors@2.3.1: 639 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 640 | 641 | json-schema-traverse@0.4.1: 642 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 643 | 644 | json-schema-traverse@1.0.0: 645 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 646 | 647 | loader-runner@4.3.0: 648 | resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} 649 | engines: {node: '>=6.11.5'} 650 | 651 | lru-cache@10.4.3: 652 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 653 | 654 | magic-string@0.30.17: 655 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} 656 | 657 | merge-anything@6.0.3: 658 | resolution: {integrity: sha512-YdazkjSuAhOSmBDccgnMILrfVXFdnzsGfrFPFdvpFTZQwsqD6RcO0E+9GlvssVRCnMVTHi/iGq0rN1z/vnmkxw==} 659 | engines: {node: '>=18'} 660 | 661 | merge-stream@2.0.0: 662 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 663 | 664 | merge2@1.4.1: 665 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 666 | engines: {node: '>= 8'} 667 | 668 | micromatch@4.0.8: 669 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 670 | engines: {node: '>=8.6'} 671 | 672 | mime-db@1.52.0: 673 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 674 | engines: {node: '>= 0.6'} 675 | 676 | mime-types@2.1.35: 677 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 678 | engines: {node: '>= 0.6'} 679 | 680 | minimatch@3.1.2: 681 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 682 | 683 | minimatch@9.0.5: 684 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 685 | engines: {node: '>=16 || 14 >=14.17'} 686 | 687 | minipass@7.1.2: 688 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 689 | engines: {node: '>=16 || 14 >=14.17'} 690 | 691 | neo-async@2.6.2: 692 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 693 | 694 | node-releases@2.0.19: 695 | resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 696 | 697 | once@1.4.0: 698 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 699 | 700 | p-map@3.0.0: 701 | resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} 702 | engines: {node: '>=8'} 703 | 704 | package-json-from-dist@1.0.1: 705 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 706 | 707 | path-is-absolute@1.0.1: 708 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 709 | engines: {node: '>=0.10.0'} 710 | 711 | path-key@3.1.1: 712 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 713 | engines: {node: '>=8'} 714 | 715 | path-parse@1.0.7: 716 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 717 | 718 | path-scurry@1.11.1: 719 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 720 | engines: {node: '>=16 || 14 >=14.18'} 721 | 722 | path-type@4.0.0: 723 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 724 | engines: {node: '>=8'} 725 | 726 | picocolors@1.1.1: 727 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 728 | 729 | picomatch@2.3.1: 730 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 731 | engines: {node: '>=8.6'} 732 | 733 | picomatch@4.0.2: 734 | resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 735 | engines: {node: '>=12'} 736 | 737 | punycode@2.3.1: 738 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 739 | engines: {node: '>=6'} 740 | 741 | queue-microtask@1.2.3: 742 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 743 | 744 | randombytes@2.1.0: 745 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 746 | 747 | react-icons@5.4.0: 748 | resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==} 749 | peerDependencies: 750 | react: '*' 751 | 752 | react@19.0.0: 753 | resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} 754 | engines: {node: '>=0.10.0'} 755 | 756 | require-from-string@2.0.2: 757 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 758 | engines: {node: '>=0.10.0'} 759 | 760 | resolve@1.22.10: 761 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 762 | engines: {node: '>= 0.4'} 763 | hasBin: true 764 | 765 | reusify@1.0.4: 766 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 767 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 768 | 769 | rimraf@3.0.2: 770 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 771 | deprecated: Rimraf versions prior to v4 are no longer supported 772 | hasBin: true 773 | 774 | rollup-plugin-delete@2.1.0: 775 | resolution: {integrity: sha512-TEbqJd7giLvzQDTu4jSPufwhTJs/iYVN2LfR/YIYkqjC/oZ0/h9Q0AeljifIhzBzJYZtHQTWKEbMms5fbh54pw==} 776 | engines: {node: '>=10'} 777 | peerDependencies: 778 | rollup: '*' 779 | 780 | rollup-plugin-external-globals@0.11.0: 781 | resolution: {integrity: sha512-LR+sH2WkgWMPxsA5o5rT7uW7BeWXSeygLe60QQi9qoN/ufaCuHDaVOIbndIkqDPnZt/wZugJh5DCzkZFdSWlLQ==} 782 | peerDependencies: 783 | rollup: ^2.25.0 || ^3.3.0 || ^4.1.4 784 | 785 | rollup-plugin-import-assets@1.1.1: 786 | resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} 787 | peerDependencies: 788 | rollup: '>=1.9.0' 789 | 790 | rollup-pluginutils@2.8.2: 791 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 792 | 793 | rollup@4.34.6: 794 | resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} 795 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 796 | hasBin: true 797 | 798 | run-parallel@1.2.0: 799 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 800 | 801 | safe-buffer@5.2.1: 802 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 803 | 804 | schema-utils@3.3.0: 805 | resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} 806 | engines: {node: '>= 10.13.0'} 807 | 808 | schema-utils@4.3.0: 809 | resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} 810 | engines: {node: '>= 10.13.0'} 811 | 812 | serialize-javascript@6.0.2: 813 | resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} 814 | 815 | shebang-command@2.0.0: 816 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 817 | engines: {node: '>=8'} 818 | 819 | shebang-regex@3.0.0: 820 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 821 | engines: {node: '>=8'} 822 | 823 | signal-exit@4.1.0: 824 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 825 | engines: {node: '>=14'} 826 | 827 | slash@3.0.0: 828 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 829 | engines: {node: '>=8'} 830 | 831 | source-map-support@0.5.21: 832 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 833 | 834 | source-map@0.6.1: 835 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 836 | engines: {node: '>=0.10.0'} 837 | 838 | string-width@4.2.3: 839 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 840 | engines: {node: '>=8'} 841 | 842 | string-width@5.1.2: 843 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 844 | engines: {node: '>=12'} 845 | 846 | strip-ansi@6.0.1: 847 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 848 | engines: {node: '>=8'} 849 | 850 | strip-ansi@7.1.0: 851 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 852 | engines: {node: '>=12'} 853 | 854 | supports-color@8.1.1: 855 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 856 | engines: {node: '>=10'} 857 | 858 | supports-preserve-symlinks-flag@1.0.0: 859 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 860 | engines: {node: '>= 0.4'} 861 | 862 | tapable@2.2.1: 863 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 864 | engines: {node: '>=6'} 865 | 866 | terser-webpack-plugin@5.3.11: 867 | resolution: {integrity: sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==} 868 | engines: {node: '>= 10.13.0'} 869 | peerDependencies: 870 | '@swc/core': '*' 871 | esbuild: '*' 872 | uglify-js: '*' 873 | webpack: ^5.1.0 874 | peerDependenciesMeta: 875 | '@swc/core': 876 | optional: true 877 | esbuild: 878 | optional: true 879 | uglify-js: 880 | optional: true 881 | 882 | terser@5.38.1: 883 | resolution: {integrity: sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==} 884 | engines: {node: '>=10'} 885 | hasBin: true 886 | 887 | to-regex-range@5.0.1: 888 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 889 | engines: {node: '>=8.0'} 890 | 891 | tslib@2.8.1: 892 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 893 | 894 | typescript@5.7.3: 895 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 896 | engines: {node: '>=14.17'} 897 | hasBin: true 898 | 899 | undici-types@6.20.0: 900 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 901 | 902 | update-browserslist-db@1.1.2: 903 | resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} 904 | hasBin: true 905 | peerDependencies: 906 | browserslist: '>= 4.21.0' 907 | 908 | uri-js@4.4.1: 909 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 910 | 911 | url-join@4.0.1: 912 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 913 | 914 | watchpack@2.4.2: 915 | resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} 916 | engines: {node: '>=10.13.0'} 917 | 918 | webpack-sources@3.2.3: 919 | resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} 920 | engines: {node: '>=10.13.0'} 921 | 922 | webpack@5.97.1: 923 | resolution: {integrity: sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==} 924 | engines: {node: '>=10.13.0'} 925 | hasBin: true 926 | peerDependencies: 927 | webpack-cli: '*' 928 | peerDependenciesMeta: 929 | webpack-cli: 930 | optional: true 931 | 932 | which@2.0.2: 933 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 934 | engines: {node: '>= 8'} 935 | hasBin: true 936 | 937 | wrap-ansi@7.0.0: 938 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 939 | engines: {node: '>=10'} 940 | 941 | wrap-ansi@8.1.0: 942 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 943 | engines: {node: '>=12'} 944 | 945 | wrappy@1.0.2: 946 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 947 | 948 | snapshots: 949 | 950 | '@decky/api@1.1.3': {} 951 | 952 | '@decky/rollup@1.0.2': 953 | dependencies: 954 | '@rollup/plugin-commonjs': 26.0.3(rollup@4.34.6) 955 | '@rollup/plugin-json': 6.1.0(rollup@4.34.6) 956 | '@rollup/plugin-node-resolve': 15.3.1(rollup@4.34.6) 957 | '@rollup/plugin-replace': 5.0.7(rollup@4.34.6) 958 | '@rollup/plugin-typescript': 11.1.6(rollup@4.34.6)(tslib@2.8.1)(typescript@5.7.3) 959 | merge-anything: 6.0.3 960 | rollup: 4.34.6 961 | rollup-plugin-delete: 2.1.0(rollup@4.34.6) 962 | rollup-plugin-external-globals: 0.11.0(rollup@4.34.6) 963 | rollup-plugin-import-assets: 1.1.1(rollup@4.34.6) 964 | tslib: 2.8.1 965 | typescript: 5.7.3 966 | 967 | '@decky/ui@4.11.0': {} 968 | 969 | '@isaacs/cliui@8.0.2': 970 | dependencies: 971 | string-width: 5.1.2 972 | string-width-cjs: string-width@4.2.3 973 | strip-ansi: 7.1.0 974 | strip-ansi-cjs: strip-ansi@6.0.1 975 | wrap-ansi: 8.1.0 976 | wrap-ansi-cjs: wrap-ansi@7.0.0 977 | 978 | '@jridgewell/gen-mapping@0.3.8': 979 | dependencies: 980 | '@jridgewell/set-array': 1.2.1 981 | '@jridgewell/sourcemap-codec': 1.5.0 982 | '@jridgewell/trace-mapping': 0.3.25 983 | 984 | '@jridgewell/resolve-uri@3.1.2': {} 985 | 986 | '@jridgewell/set-array@1.2.1': {} 987 | 988 | '@jridgewell/source-map@0.3.6': 989 | dependencies: 990 | '@jridgewell/gen-mapping': 0.3.8 991 | '@jridgewell/trace-mapping': 0.3.25 992 | 993 | '@jridgewell/sourcemap-codec@1.5.0': {} 994 | 995 | '@jridgewell/trace-mapping@0.3.25': 996 | dependencies: 997 | '@jridgewell/resolve-uri': 3.1.2 998 | '@jridgewell/sourcemap-codec': 1.5.0 999 | 1000 | '@nodelib/fs.scandir@2.1.5': 1001 | dependencies: 1002 | '@nodelib/fs.stat': 2.0.5 1003 | run-parallel: 1.2.0 1004 | 1005 | '@nodelib/fs.stat@2.0.5': {} 1006 | 1007 | '@nodelib/fs.walk@1.2.8': 1008 | dependencies: 1009 | '@nodelib/fs.scandir': 2.1.5 1010 | fastq: 1.19.0 1011 | 1012 | '@pkgjs/parseargs@0.11.0': 1013 | optional: true 1014 | 1015 | '@rollup/plugin-commonjs@26.0.3(rollup@4.34.6)': 1016 | dependencies: 1017 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1018 | commondir: 1.0.1 1019 | estree-walker: 2.0.2 1020 | glob: 10.4.5 1021 | is-reference: 1.2.1 1022 | magic-string: 0.30.17 1023 | optionalDependencies: 1024 | rollup: 4.34.6 1025 | 1026 | '@rollup/plugin-json@6.1.0(rollup@4.34.6)': 1027 | dependencies: 1028 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1029 | optionalDependencies: 1030 | rollup: 4.34.6 1031 | 1032 | '@rollup/plugin-node-resolve@15.3.1(rollup@4.34.6)': 1033 | dependencies: 1034 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1035 | '@types/resolve': 1.20.2 1036 | deepmerge: 4.3.1 1037 | is-module: 1.0.0 1038 | resolve: 1.22.10 1039 | optionalDependencies: 1040 | rollup: 4.34.6 1041 | 1042 | '@rollup/plugin-replace@5.0.7(rollup@4.34.6)': 1043 | dependencies: 1044 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1045 | magic-string: 0.30.17 1046 | optionalDependencies: 1047 | rollup: 4.34.6 1048 | 1049 | '@rollup/plugin-typescript@11.1.6(rollup@4.34.6)(tslib@2.8.1)(typescript@5.7.3)': 1050 | dependencies: 1051 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1052 | resolve: 1.22.10 1053 | typescript: 5.7.3 1054 | optionalDependencies: 1055 | rollup: 4.34.6 1056 | tslib: 2.8.1 1057 | 1058 | '@rollup/pluginutils@5.1.4(rollup@4.34.6)': 1059 | dependencies: 1060 | '@types/estree': 1.0.6 1061 | estree-walker: 2.0.2 1062 | picomatch: 4.0.2 1063 | optionalDependencies: 1064 | rollup: 4.34.6 1065 | 1066 | '@rollup/rollup-android-arm-eabi@4.34.6': 1067 | optional: true 1068 | 1069 | '@rollup/rollup-android-arm64@4.34.6': 1070 | optional: true 1071 | 1072 | '@rollup/rollup-darwin-arm64@4.34.6': 1073 | optional: true 1074 | 1075 | '@rollup/rollup-darwin-x64@4.34.6': 1076 | optional: true 1077 | 1078 | '@rollup/rollup-freebsd-arm64@4.34.6': 1079 | optional: true 1080 | 1081 | '@rollup/rollup-freebsd-x64@4.34.6': 1082 | optional: true 1083 | 1084 | '@rollup/rollup-linux-arm-gnueabihf@4.34.6': 1085 | optional: true 1086 | 1087 | '@rollup/rollup-linux-arm-musleabihf@4.34.6': 1088 | optional: true 1089 | 1090 | '@rollup/rollup-linux-arm64-gnu@4.34.6': 1091 | optional: true 1092 | 1093 | '@rollup/rollup-linux-arm64-musl@4.34.6': 1094 | optional: true 1095 | 1096 | '@rollup/rollup-linux-loongarch64-gnu@4.34.6': 1097 | optional: true 1098 | 1099 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': 1100 | optional: true 1101 | 1102 | '@rollup/rollup-linux-riscv64-gnu@4.34.6': 1103 | optional: true 1104 | 1105 | '@rollup/rollup-linux-s390x-gnu@4.34.6': 1106 | optional: true 1107 | 1108 | '@rollup/rollup-linux-x64-gnu@4.34.6': 1109 | optional: true 1110 | 1111 | '@rollup/rollup-linux-x64-musl@4.34.6': 1112 | optional: true 1113 | 1114 | '@rollup/rollup-win32-arm64-msvc@4.34.6': 1115 | optional: true 1116 | 1117 | '@rollup/rollup-win32-ia32-msvc@4.34.6': 1118 | optional: true 1119 | 1120 | '@rollup/rollup-win32-x64-msvc@4.34.6': 1121 | optional: true 1122 | 1123 | '@types/eslint-scope@3.7.7': 1124 | dependencies: 1125 | '@types/eslint': 9.6.1 1126 | '@types/estree': 1.0.6 1127 | 1128 | '@types/eslint@9.6.1': 1129 | dependencies: 1130 | '@types/estree': 1.0.6 1131 | '@types/json-schema': 7.0.15 1132 | 1133 | '@types/estree@1.0.6': {} 1134 | 1135 | '@types/glob@7.2.0': 1136 | dependencies: 1137 | '@types/minimatch': 5.1.2 1138 | '@types/node': 22.13.1 1139 | 1140 | '@types/json-schema@7.0.15': {} 1141 | 1142 | '@types/minimatch@5.1.2': {} 1143 | 1144 | '@types/node@22.13.1': 1145 | dependencies: 1146 | undici-types: 6.20.0 1147 | 1148 | '@types/prop-types@15.7.14': {} 1149 | 1150 | '@types/react-dom@18.3.0': 1151 | dependencies: 1152 | '@types/react': 18.3.3 1153 | 1154 | '@types/react@18.3.3': 1155 | dependencies: 1156 | '@types/prop-types': 15.7.14 1157 | csstype: 3.1.3 1158 | 1159 | '@types/resolve@1.20.2': {} 1160 | 1161 | '@types/webpack@5.28.5': 1162 | dependencies: 1163 | '@types/node': 22.13.1 1164 | tapable: 2.2.1 1165 | webpack: 5.97.1 1166 | transitivePeerDependencies: 1167 | - '@swc/core' 1168 | - esbuild 1169 | - uglify-js 1170 | - webpack-cli 1171 | 1172 | '@webassemblyjs/ast@1.14.1': 1173 | dependencies: 1174 | '@webassemblyjs/helper-numbers': 1.13.2 1175 | '@webassemblyjs/helper-wasm-bytecode': 1.13.2 1176 | 1177 | '@webassemblyjs/floating-point-hex-parser@1.13.2': {} 1178 | 1179 | '@webassemblyjs/helper-api-error@1.13.2': {} 1180 | 1181 | '@webassemblyjs/helper-buffer@1.14.1': {} 1182 | 1183 | '@webassemblyjs/helper-numbers@1.13.2': 1184 | dependencies: 1185 | '@webassemblyjs/floating-point-hex-parser': 1.13.2 1186 | '@webassemblyjs/helper-api-error': 1.13.2 1187 | '@xtuc/long': 4.2.2 1188 | 1189 | '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} 1190 | 1191 | '@webassemblyjs/helper-wasm-section@1.14.1': 1192 | dependencies: 1193 | '@webassemblyjs/ast': 1.14.1 1194 | '@webassemblyjs/helper-buffer': 1.14.1 1195 | '@webassemblyjs/helper-wasm-bytecode': 1.13.2 1196 | '@webassemblyjs/wasm-gen': 1.14.1 1197 | 1198 | '@webassemblyjs/ieee754@1.13.2': 1199 | dependencies: 1200 | '@xtuc/ieee754': 1.2.0 1201 | 1202 | '@webassemblyjs/leb128@1.13.2': 1203 | dependencies: 1204 | '@xtuc/long': 4.2.2 1205 | 1206 | '@webassemblyjs/utf8@1.13.2': {} 1207 | 1208 | '@webassemblyjs/wasm-edit@1.14.1': 1209 | dependencies: 1210 | '@webassemblyjs/ast': 1.14.1 1211 | '@webassemblyjs/helper-buffer': 1.14.1 1212 | '@webassemblyjs/helper-wasm-bytecode': 1.13.2 1213 | '@webassemblyjs/helper-wasm-section': 1.14.1 1214 | '@webassemblyjs/wasm-gen': 1.14.1 1215 | '@webassemblyjs/wasm-opt': 1.14.1 1216 | '@webassemblyjs/wasm-parser': 1.14.1 1217 | '@webassemblyjs/wast-printer': 1.14.1 1218 | 1219 | '@webassemblyjs/wasm-gen@1.14.1': 1220 | dependencies: 1221 | '@webassemblyjs/ast': 1.14.1 1222 | '@webassemblyjs/helper-wasm-bytecode': 1.13.2 1223 | '@webassemblyjs/ieee754': 1.13.2 1224 | '@webassemblyjs/leb128': 1.13.2 1225 | '@webassemblyjs/utf8': 1.13.2 1226 | 1227 | '@webassemblyjs/wasm-opt@1.14.1': 1228 | dependencies: 1229 | '@webassemblyjs/ast': 1.14.1 1230 | '@webassemblyjs/helper-buffer': 1.14.1 1231 | '@webassemblyjs/wasm-gen': 1.14.1 1232 | '@webassemblyjs/wasm-parser': 1.14.1 1233 | 1234 | '@webassemblyjs/wasm-parser@1.14.1': 1235 | dependencies: 1236 | '@webassemblyjs/ast': 1.14.1 1237 | '@webassemblyjs/helper-api-error': 1.13.2 1238 | '@webassemblyjs/helper-wasm-bytecode': 1.13.2 1239 | '@webassemblyjs/ieee754': 1.13.2 1240 | '@webassemblyjs/leb128': 1.13.2 1241 | '@webassemblyjs/utf8': 1.13.2 1242 | 1243 | '@webassemblyjs/wast-printer@1.14.1': 1244 | dependencies: 1245 | '@webassemblyjs/ast': 1.14.1 1246 | '@xtuc/long': 4.2.2 1247 | 1248 | '@xtuc/ieee754@1.2.0': {} 1249 | 1250 | '@xtuc/long@4.2.2': {} 1251 | 1252 | acorn@8.14.0: {} 1253 | 1254 | aggregate-error@3.1.0: 1255 | dependencies: 1256 | clean-stack: 2.2.0 1257 | indent-string: 4.0.0 1258 | 1259 | ajv-formats@2.1.1(ajv@8.17.1): 1260 | optionalDependencies: 1261 | ajv: 8.17.1 1262 | 1263 | ajv-keywords@3.5.2(ajv@6.12.6): 1264 | dependencies: 1265 | ajv: 6.12.6 1266 | 1267 | ajv-keywords@5.1.0(ajv@8.17.1): 1268 | dependencies: 1269 | ajv: 8.17.1 1270 | fast-deep-equal: 3.1.3 1271 | 1272 | ajv@6.12.6: 1273 | dependencies: 1274 | fast-deep-equal: 3.1.3 1275 | fast-json-stable-stringify: 2.1.0 1276 | json-schema-traverse: 0.4.1 1277 | uri-js: 4.4.1 1278 | 1279 | ajv@8.17.1: 1280 | dependencies: 1281 | fast-deep-equal: 3.1.3 1282 | fast-uri: 3.0.6 1283 | json-schema-traverse: 1.0.0 1284 | require-from-string: 2.0.2 1285 | 1286 | ansi-regex@5.0.1: {} 1287 | 1288 | ansi-regex@6.1.0: {} 1289 | 1290 | ansi-styles@4.3.0: 1291 | dependencies: 1292 | color-convert: 2.0.1 1293 | 1294 | ansi-styles@6.2.1: {} 1295 | 1296 | array-union@2.1.0: {} 1297 | 1298 | balanced-match@1.0.2: {} 1299 | 1300 | brace-expansion@1.1.11: 1301 | dependencies: 1302 | balanced-match: 1.0.2 1303 | concat-map: 0.0.1 1304 | 1305 | brace-expansion@2.0.1: 1306 | dependencies: 1307 | balanced-match: 1.0.2 1308 | 1309 | braces@3.0.3: 1310 | dependencies: 1311 | fill-range: 7.1.1 1312 | 1313 | browserslist@4.24.4: 1314 | dependencies: 1315 | caniuse-lite: 1.0.30001699 1316 | electron-to-chromium: 1.5.96 1317 | node-releases: 2.0.19 1318 | update-browserslist-db: 1.1.2(browserslist@4.24.4) 1319 | 1320 | buffer-from@1.1.2: {} 1321 | 1322 | caniuse-lite@1.0.30001699: {} 1323 | 1324 | chrome-trace-event@1.0.4: {} 1325 | 1326 | clean-stack@2.2.0: {} 1327 | 1328 | color-convert@2.0.1: 1329 | dependencies: 1330 | color-name: 1.1.4 1331 | 1332 | color-name@1.1.4: {} 1333 | 1334 | commander@2.20.3: {} 1335 | 1336 | commondir@1.0.1: {} 1337 | 1338 | concat-map@0.0.1: {} 1339 | 1340 | cross-spawn@7.0.6: 1341 | dependencies: 1342 | path-key: 3.1.1 1343 | shebang-command: 2.0.0 1344 | which: 2.0.2 1345 | 1346 | csstype@3.1.3: {} 1347 | 1348 | deepmerge@4.3.1: {} 1349 | 1350 | del@5.1.0: 1351 | dependencies: 1352 | globby: 10.0.2 1353 | graceful-fs: 4.2.11 1354 | is-glob: 4.0.3 1355 | is-path-cwd: 2.2.0 1356 | is-path-inside: 3.0.3 1357 | p-map: 3.0.0 1358 | rimraf: 3.0.2 1359 | slash: 3.0.0 1360 | 1361 | dir-glob@3.0.1: 1362 | dependencies: 1363 | path-type: 4.0.0 1364 | 1365 | eastasianwidth@0.2.0: {} 1366 | 1367 | electron-to-chromium@1.5.96: {} 1368 | 1369 | emoji-regex@8.0.0: {} 1370 | 1371 | emoji-regex@9.2.2: {} 1372 | 1373 | enhanced-resolve@5.18.1: 1374 | dependencies: 1375 | graceful-fs: 4.2.11 1376 | tapable: 2.2.1 1377 | 1378 | es-module-lexer@1.6.0: {} 1379 | 1380 | escalade@3.2.0: {} 1381 | 1382 | eslint-scope@5.1.1: 1383 | dependencies: 1384 | esrecurse: 4.3.0 1385 | estraverse: 4.3.0 1386 | 1387 | esrecurse@4.3.0: 1388 | dependencies: 1389 | estraverse: 5.3.0 1390 | 1391 | estraverse@4.3.0: {} 1392 | 1393 | estraverse@5.3.0: {} 1394 | 1395 | estree-walker@0.6.1: {} 1396 | 1397 | estree-walker@2.0.2: {} 1398 | 1399 | estree-walker@3.0.3: 1400 | dependencies: 1401 | '@types/estree': 1.0.6 1402 | 1403 | events@3.3.0: {} 1404 | 1405 | fast-deep-equal@3.1.3: {} 1406 | 1407 | fast-glob@3.3.3: 1408 | dependencies: 1409 | '@nodelib/fs.stat': 2.0.5 1410 | '@nodelib/fs.walk': 1.2.8 1411 | glob-parent: 5.1.2 1412 | merge2: 1.4.1 1413 | micromatch: 4.0.8 1414 | 1415 | fast-json-stable-stringify@2.1.0: {} 1416 | 1417 | fast-uri@3.0.6: {} 1418 | 1419 | fastq@1.19.0: 1420 | dependencies: 1421 | reusify: 1.0.4 1422 | 1423 | fill-range@7.1.1: 1424 | dependencies: 1425 | to-regex-range: 5.0.1 1426 | 1427 | foreground-child@3.3.0: 1428 | dependencies: 1429 | cross-spawn: 7.0.6 1430 | signal-exit: 4.1.0 1431 | 1432 | fs.realpath@1.0.0: {} 1433 | 1434 | fsevents@2.3.3: 1435 | optional: true 1436 | 1437 | function-bind@1.1.2: {} 1438 | 1439 | glob-parent@5.1.2: 1440 | dependencies: 1441 | is-glob: 4.0.3 1442 | 1443 | glob-to-regexp@0.4.1: {} 1444 | 1445 | glob@10.4.5: 1446 | dependencies: 1447 | foreground-child: 3.3.0 1448 | jackspeak: 3.4.3 1449 | minimatch: 9.0.5 1450 | minipass: 7.1.2 1451 | package-json-from-dist: 1.0.1 1452 | path-scurry: 1.11.1 1453 | 1454 | glob@7.2.3: 1455 | dependencies: 1456 | fs.realpath: 1.0.0 1457 | inflight: 1.0.6 1458 | inherits: 2.0.4 1459 | minimatch: 3.1.2 1460 | once: 1.4.0 1461 | path-is-absolute: 1.0.1 1462 | 1463 | globby@10.0.2: 1464 | dependencies: 1465 | '@types/glob': 7.2.0 1466 | array-union: 2.1.0 1467 | dir-glob: 3.0.1 1468 | fast-glob: 3.3.3 1469 | glob: 7.2.3 1470 | ignore: 5.3.2 1471 | merge2: 1.4.1 1472 | slash: 3.0.0 1473 | 1474 | graceful-fs@4.2.11: {} 1475 | 1476 | has-flag@4.0.0: {} 1477 | 1478 | hasown@2.0.2: 1479 | dependencies: 1480 | function-bind: 1.1.2 1481 | 1482 | ignore@5.3.2: {} 1483 | 1484 | indent-string@4.0.0: {} 1485 | 1486 | inflight@1.0.6: 1487 | dependencies: 1488 | once: 1.4.0 1489 | wrappy: 1.0.2 1490 | 1491 | inherits@2.0.4: {} 1492 | 1493 | is-core-module@2.16.1: 1494 | dependencies: 1495 | hasown: 2.0.2 1496 | 1497 | is-extglob@2.1.1: {} 1498 | 1499 | is-fullwidth-code-point@3.0.0: {} 1500 | 1501 | is-glob@4.0.3: 1502 | dependencies: 1503 | is-extglob: 2.1.1 1504 | 1505 | is-module@1.0.0: {} 1506 | 1507 | is-number@7.0.0: {} 1508 | 1509 | is-path-cwd@2.2.0: {} 1510 | 1511 | is-path-inside@3.0.3: {} 1512 | 1513 | is-reference@1.2.1: 1514 | dependencies: 1515 | '@types/estree': 1.0.6 1516 | 1517 | is-reference@3.0.3: 1518 | dependencies: 1519 | '@types/estree': 1.0.6 1520 | 1521 | is-what@5.0.2: {} 1522 | 1523 | isexe@2.0.0: {} 1524 | 1525 | jackspeak@3.4.3: 1526 | dependencies: 1527 | '@isaacs/cliui': 8.0.2 1528 | optionalDependencies: 1529 | '@pkgjs/parseargs': 0.11.0 1530 | 1531 | jest-worker@27.5.1: 1532 | dependencies: 1533 | '@types/node': 22.13.1 1534 | merge-stream: 2.0.0 1535 | supports-color: 8.1.1 1536 | 1537 | json-parse-even-better-errors@2.3.1: {} 1538 | 1539 | json-schema-traverse@0.4.1: {} 1540 | 1541 | json-schema-traverse@1.0.0: {} 1542 | 1543 | loader-runner@4.3.0: {} 1544 | 1545 | lru-cache@10.4.3: {} 1546 | 1547 | magic-string@0.30.17: 1548 | dependencies: 1549 | '@jridgewell/sourcemap-codec': 1.5.0 1550 | 1551 | merge-anything@6.0.3: 1552 | dependencies: 1553 | is-what: 5.0.2 1554 | 1555 | merge-stream@2.0.0: {} 1556 | 1557 | merge2@1.4.1: {} 1558 | 1559 | micromatch@4.0.8: 1560 | dependencies: 1561 | braces: 3.0.3 1562 | picomatch: 2.3.1 1563 | 1564 | mime-db@1.52.0: {} 1565 | 1566 | mime-types@2.1.35: 1567 | dependencies: 1568 | mime-db: 1.52.0 1569 | 1570 | minimatch@3.1.2: 1571 | dependencies: 1572 | brace-expansion: 1.1.11 1573 | 1574 | minimatch@9.0.5: 1575 | dependencies: 1576 | brace-expansion: 2.0.1 1577 | 1578 | minipass@7.1.2: {} 1579 | 1580 | neo-async@2.6.2: {} 1581 | 1582 | node-releases@2.0.19: {} 1583 | 1584 | once@1.4.0: 1585 | dependencies: 1586 | wrappy: 1.0.2 1587 | 1588 | p-map@3.0.0: 1589 | dependencies: 1590 | aggregate-error: 3.1.0 1591 | 1592 | package-json-from-dist@1.0.1: {} 1593 | 1594 | path-is-absolute@1.0.1: {} 1595 | 1596 | path-key@3.1.1: {} 1597 | 1598 | path-parse@1.0.7: {} 1599 | 1600 | path-scurry@1.11.1: 1601 | dependencies: 1602 | lru-cache: 10.4.3 1603 | minipass: 7.1.2 1604 | 1605 | path-type@4.0.0: {} 1606 | 1607 | picocolors@1.1.1: {} 1608 | 1609 | picomatch@2.3.1: {} 1610 | 1611 | picomatch@4.0.2: {} 1612 | 1613 | punycode@2.3.1: {} 1614 | 1615 | queue-microtask@1.2.3: {} 1616 | 1617 | randombytes@2.1.0: 1618 | dependencies: 1619 | safe-buffer: 5.2.1 1620 | 1621 | react-icons@5.4.0(react@19.0.0): 1622 | dependencies: 1623 | react: 19.0.0 1624 | 1625 | react@19.0.0: {} 1626 | 1627 | require-from-string@2.0.2: {} 1628 | 1629 | resolve@1.22.10: 1630 | dependencies: 1631 | is-core-module: 2.16.1 1632 | path-parse: 1.0.7 1633 | supports-preserve-symlinks-flag: 1.0.0 1634 | 1635 | reusify@1.0.4: {} 1636 | 1637 | rimraf@3.0.2: 1638 | dependencies: 1639 | glob: 7.2.3 1640 | 1641 | rollup-plugin-delete@2.1.0(rollup@4.34.6): 1642 | dependencies: 1643 | del: 5.1.0 1644 | rollup: 4.34.6 1645 | 1646 | rollup-plugin-external-globals@0.11.0(rollup@4.34.6): 1647 | dependencies: 1648 | '@rollup/pluginutils': 5.1.4(rollup@4.34.6) 1649 | estree-walker: 3.0.3 1650 | is-reference: 3.0.3 1651 | magic-string: 0.30.17 1652 | rollup: 4.34.6 1653 | 1654 | rollup-plugin-import-assets@1.1.1(rollup@4.34.6): 1655 | dependencies: 1656 | rollup: 4.34.6 1657 | rollup-pluginutils: 2.8.2 1658 | url-join: 4.0.1 1659 | 1660 | rollup-pluginutils@2.8.2: 1661 | dependencies: 1662 | estree-walker: 0.6.1 1663 | 1664 | rollup@4.34.6: 1665 | dependencies: 1666 | '@types/estree': 1.0.6 1667 | optionalDependencies: 1668 | '@rollup/rollup-android-arm-eabi': 4.34.6 1669 | '@rollup/rollup-android-arm64': 4.34.6 1670 | '@rollup/rollup-darwin-arm64': 4.34.6 1671 | '@rollup/rollup-darwin-x64': 4.34.6 1672 | '@rollup/rollup-freebsd-arm64': 4.34.6 1673 | '@rollup/rollup-freebsd-x64': 4.34.6 1674 | '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 1675 | '@rollup/rollup-linux-arm-musleabihf': 4.34.6 1676 | '@rollup/rollup-linux-arm64-gnu': 4.34.6 1677 | '@rollup/rollup-linux-arm64-musl': 4.34.6 1678 | '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 1679 | '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 1680 | '@rollup/rollup-linux-riscv64-gnu': 4.34.6 1681 | '@rollup/rollup-linux-s390x-gnu': 4.34.6 1682 | '@rollup/rollup-linux-x64-gnu': 4.34.6 1683 | '@rollup/rollup-linux-x64-musl': 4.34.6 1684 | '@rollup/rollup-win32-arm64-msvc': 4.34.6 1685 | '@rollup/rollup-win32-ia32-msvc': 4.34.6 1686 | '@rollup/rollup-win32-x64-msvc': 4.34.6 1687 | fsevents: 2.3.3 1688 | 1689 | run-parallel@1.2.0: 1690 | dependencies: 1691 | queue-microtask: 1.2.3 1692 | 1693 | safe-buffer@5.2.1: {} 1694 | 1695 | schema-utils@3.3.0: 1696 | dependencies: 1697 | '@types/json-schema': 7.0.15 1698 | ajv: 6.12.6 1699 | ajv-keywords: 3.5.2(ajv@6.12.6) 1700 | 1701 | schema-utils@4.3.0: 1702 | dependencies: 1703 | '@types/json-schema': 7.0.15 1704 | ajv: 8.17.1 1705 | ajv-formats: 2.1.1(ajv@8.17.1) 1706 | ajv-keywords: 5.1.0(ajv@8.17.1) 1707 | 1708 | serialize-javascript@6.0.2: 1709 | dependencies: 1710 | randombytes: 2.1.0 1711 | 1712 | shebang-command@2.0.0: 1713 | dependencies: 1714 | shebang-regex: 3.0.0 1715 | 1716 | shebang-regex@3.0.0: {} 1717 | 1718 | signal-exit@4.1.0: {} 1719 | 1720 | slash@3.0.0: {} 1721 | 1722 | source-map-support@0.5.21: 1723 | dependencies: 1724 | buffer-from: 1.1.2 1725 | source-map: 0.6.1 1726 | 1727 | source-map@0.6.1: {} 1728 | 1729 | string-width@4.2.3: 1730 | dependencies: 1731 | emoji-regex: 8.0.0 1732 | is-fullwidth-code-point: 3.0.0 1733 | strip-ansi: 6.0.1 1734 | 1735 | string-width@5.1.2: 1736 | dependencies: 1737 | eastasianwidth: 0.2.0 1738 | emoji-regex: 9.2.2 1739 | strip-ansi: 7.1.0 1740 | 1741 | strip-ansi@6.0.1: 1742 | dependencies: 1743 | ansi-regex: 5.0.1 1744 | 1745 | strip-ansi@7.1.0: 1746 | dependencies: 1747 | ansi-regex: 6.1.0 1748 | 1749 | supports-color@8.1.1: 1750 | dependencies: 1751 | has-flag: 4.0.0 1752 | 1753 | supports-preserve-symlinks-flag@1.0.0: {} 1754 | 1755 | tapable@2.2.1: {} 1756 | 1757 | terser-webpack-plugin@5.3.11(webpack@5.97.1): 1758 | dependencies: 1759 | '@jridgewell/trace-mapping': 0.3.25 1760 | jest-worker: 27.5.1 1761 | schema-utils: 4.3.0 1762 | serialize-javascript: 6.0.2 1763 | terser: 5.38.1 1764 | webpack: 5.97.1 1765 | 1766 | terser@5.38.1: 1767 | dependencies: 1768 | '@jridgewell/source-map': 0.3.6 1769 | acorn: 8.14.0 1770 | commander: 2.20.3 1771 | source-map-support: 0.5.21 1772 | 1773 | to-regex-range@5.0.1: 1774 | dependencies: 1775 | is-number: 7.0.0 1776 | 1777 | tslib@2.8.1: {} 1778 | 1779 | typescript@5.7.3: {} 1780 | 1781 | undici-types@6.20.0: {} 1782 | 1783 | update-browserslist-db@1.1.2(browserslist@4.24.4): 1784 | dependencies: 1785 | browserslist: 4.24.4 1786 | escalade: 3.2.0 1787 | picocolors: 1.1.1 1788 | 1789 | uri-js@4.4.1: 1790 | dependencies: 1791 | punycode: 2.3.1 1792 | 1793 | url-join@4.0.1: {} 1794 | 1795 | watchpack@2.4.2: 1796 | dependencies: 1797 | glob-to-regexp: 0.4.1 1798 | graceful-fs: 4.2.11 1799 | 1800 | webpack-sources@3.2.3: {} 1801 | 1802 | webpack@5.97.1: 1803 | dependencies: 1804 | '@types/eslint-scope': 3.7.7 1805 | '@types/estree': 1.0.6 1806 | '@webassemblyjs/ast': 1.14.1 1807 | '@webassemblyjs/wasm-edit': 1.14.1 1808 | '@webassemblyjs/wasm-parser': 1.14.1 1809 | acorn: 8.14.0 1810 | browserslist: 4.24.4 1811 | chrome-trace-event: 1.0.4 1812 | enhanced-resolve: 5.18.1 1813 | es-module-lexer: 1.6.0 1814 | eslint-scope: 5.1.1 1815 | events: 3.3.0 1816 | glob-to-regexp: 0.4.1 1817 | graceful-fs: 4.2.11 1818 | json-parse-even-better-errors: 2.3.1 1819 | loader-runner: 4.3.0 1820 | mime-types: 2.1.35 1821 | neo-async: 2.6.2 1822 | schema-utils: 3.3.0 1823 | tapable: 2.2.1 1824 | terser-webpack-plugin: 5.3.11(webpack@5.97.1) 1825 | watchpack: 2.4.2 1826 | webpack-sources: 3.2.3 1827 | transitivePeerDependencies: 1828 | - '@swc/core' 1829 | - esbuild 1830 | - uglify-js 1831 | 1832 | which@2.0.2: 1833 | dependencies: 1834 | isexe: 2.0.0 1835 | 1836 | wrap-ansi@7.0.0: 1837 | dependencies: 1838 | ansi-styles: 4.3.0 1839 | string-width: 4.2.3 1840 | strip-ansi: 6.0.1 1841 | 1842 | wrap-ansi@8.1.0: 1843 | dependencies: 1844 | ansi-styles: 6.2.1 1845 | string-width: 5.1.2 1846 | strip-ansi: 7.1.0 1847 | 1848 | wrappy@1.0.2: {} 1849 | --------------------------------------------------------------------------------