[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: V0rt3x_workshop # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: v0r_t3x # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: ''\nassignees: ''\n\n---\n\n**Pwnagotchi Version**: ``\n**Hardware (SBC)**: ``\n**Screen Type**: ``\n**Other Hardware Used**: ``\n**Pwnagotchi Fork (if applicable)**: ``\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n\n**Logs**\n- Run the diagnostic script and attach the log archive here (if relevant).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": "Fancygotchi.py",
    "content": "# adding api or ui attribute to know the actual theme config and or fancy state\n\nimport argparse\nimport asyncio\nimport copy\nimport importlib\nimport glob\nimport gettext\nimport importlib.util\nimport json\nimport logging\nimport math\nimport numpy as np\nimport os\nimport random\nimport re\nimport requests\nimport secrets\nimport shutil\nimport struct\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nimport toml\nimport traceback\nimport zipfile\n\nfrom io import BytesIO\nfrom multiprocessing.connection import Client, Listener\nfrom os import system\nfrom shutil import copy2, copyfile, copytree\nfrom textwrap import TextWrapper\nfrom toml import dump, load\nfrom PIL import Image, ImageChops, ImageDraw, ImageFont, ImageOps, ImageSequence\nfrom flask import abort, jsonify, make_response, render_template_string, send_file, session\n\nimport pwnagotchi\nimport pwnagotchi.plugins as plugins\nimport pwnagotchi.ui.faces as faces\nimport pwnagotchi.ui.fonts as fonts\nfrom pwnagotchi import utils\nfrom pwnagotchi.plugins import toggle_plugin\nfrom pwnagotchi.ui import display\nfrom pwnagotchi.ui.hw import display_for\nfrom pwnagotchi.utils import load_config, merge_config, save_config\n\nV0RT3X_REPO = \"https://github.com/V0r-T3x\"\nFANCY_REPO = os.path.join(V0RT3X_REPO, \"Fancygotchi\")\nTHEMES_REPO = \"https://api.github.com/repos/V0r-T3x/Fancygotchi_themes/contents/fancygotchi_2.0/themes\"\n\n\nLOGO = \"\"\"░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒░░░░░░▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓████▓▓▓▓▓▓▓▓▓████████▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓███████▓▓▓▓▓▓▓▓██████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓█████▓▓▓▓▓▓▓▓▓▓▓██████████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓█▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓███████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓█▓▒▒▒▒▓▓▓▓▓▓▓▓▓█████████████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░▒▓▓▓█▓▓▓█▓▓██████████████████████████████▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░███████████████████████████████████████████████████▓░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░████████████████████████████████████████████████████░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░▒████████▓▓▓▓▓▓▓▓██████████████████████████████████▒░░░░▒▒▒▒░░░░░░░░░░\n░░░░░░░░░▓▓▒░░░░░░░░░░▓█████▓▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████▓░░░░░░▓▓▓▓▓▓▓▓▓░░░░░\n░░░░░░░░▒▒▒▓▒░░░░░░░░░░░▒▓██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓██▓▒░░░░░░░░░█▓▓▓▓▓█▓░░░░░░\n░░░░░░░░▓░░▒▒▓▒░░░░░░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓░░░░░░░░░░░▒▒▓▓▓▓▓█▒░░░░░░\n░░░░░░░▓▒▒▒▒▒▓▓▒░░░░░░░░░░░▓▒▒▓████▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█████▓▓▓▓░░░░░░░░░░░▒▒▒▒▒▒▓▓░░░░░░░\n░░░░░░░▒▒░░░░▒▒▓░░░░░░░░░░░▓▒▒▒██▓▓████▓▒▒▒▒▒▒▒▒▒▒▓▓████▓███▓▓▓▒░░░░░░░░░░▒▓▓▓▓▓▒▓▒░░░░░░░\n░░░░░░░░▓░░░░░▒▓░░░░░░░░░░░░▓▒▒▒███████▓▒▒▒▒▒▒▒▒▒▒▒▓███████▓▓▓▓░░░░░░░░░░░░░▓▓██▓▓░░░░░░░░\n░░░░░░░▒▓▒░░░░▓▓▒▒▒░░░░░░░░░▒▓▒▒▒▓███▓▒▒▓▓▓▓▒▒▒▓▓▓▒▒▒▓████▓▓▓▓░░░░░░░░░░░▒▒▓▓▓█░░░░░░░░░░░\n░░░░░░░▒▓▓▒▓▒▒▓▓▓█▓▓░░░░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▓▓▓▓░░░░░░░░░░░▓▒▒█▓▓▓░░░░░░░░░░░\n░░░░░░░░▒█▒▓▓▓▓▓███▓▒░░░░░░░░▒▓█▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓██▒░░░░░░░░░░░█▓▓█▓█▒▒▒░░░░░░░░░\n░░░░░░░░░▓░▓▓▓▒▒▓██▓▓▓▒░░░░▒▓███▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓████▓▒░░░░░░░░▓▓██████▓▒▓░░░░░░░░\n░░░░░░░░░▒▓▒▒▒▓▓████▓▓▓▓▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓███▓▒▒░▒▒▓▒▒▓██████▓▒▓░░░░░░░░\n░░░░░░░░░░░░░▒████▓██▓▓▓▓▓▓▓▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▓██████▓▒▓▒░░░░░░░░\n░░░░░░░░░░░░░░▒████▓▒▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓█████▓▒▒░░░░░░░░░░\n░░░░░░░░░▒██░░░▓█████▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓█▓▒▒▒▒▒▒▓█▓▓▒▒▒▒▒▒▓█▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓███▓█▓▒▒░░░░░░░░░░░\n░░░░░░░░▒██░░░░▒▒████████▓▓▓▓▓▓██████▓▒▒▒▒▒▓█████▓▒▒▒▒▒████████▓▓▓▓▓█████░▒██▓██▓░░░░░░░░░\n░░░░░░░░▓██░░░▒░░▒█████████▓▓████████▓▒▒▒▒▓███████▓▒▒▒▒████████████████▓░░░▒▒░▒██▓░░░░░░░░\n░░░░░░░░▒███▒░░░▒████▒░██▓███▓███████▒▒▒▒▓█████████▓▒▒▒▒████████▓██▓██▓░░░░░░░▒███░░░░░░░░\n░░░░░░░░░▒███████████▒▒█▓▓██▓▓▓█████▒▒▒▒▒███████████▓▒▒▒▒██████▓▓█████▓▒░░░░░░▓██▓░░░░░░░░\n░░░░░░░░░░░▒▓▓██████▓▓███▓███▓▒▒▓▓▒▒▒▒▒▓██████████████▒▒▒▒▒▓▓▒▒▒██████▓▓▓▒▒▒▓▓███▒░░░░░░░░\n░░░░░░░░░░░░░▓███████████▓▓████▓▓▒▒▒▓▓███████▓▒▓██▓█████▓▒▒▒▒▒▓████████████████▓░░░░░░░░░░\n░░░░░░░░░░░░░░▓███████████▓███████████████▒░░░░░░░▓▓██████████████▓█████████▓▒░░░░░░░░░░░░\n░░░░░░░░░░░░░░░▒▓▓██████▓░░▒█████████████▒░░░░░░░░░▓█▓███████████▒░░▓▓█▓▓▓▓▒░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░▒░░░░░░░▒▓██▓██▓███▓▒░░░░░░░░░░░░▓██▓█████▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\n░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\"\"\"\n\nINDEX = \"\"\"\n{% extends \"base.html\" %}\n{% set active_page = \"plugins\" %}\n{% block title %}\n    Fancygotchi\n{% endblock %}\n{% block meta %}\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, user-scalable=0\" />\n{% endblock %}\n{% block styles %}\n{{ super() }}\n<style>\n    body {\n        position: relative;\n        min-height: 100vh;\n    }\n    #wrap {\n        width: 100%;\n        float:left;\n        padding: 10px;\n        padding-bottom: 50px;\n        align-items: center;\n        justify-content: center;\n        flex-grow: 1;\n        display: flex;\n        border-bottom: 1px solid black;\n        flex-direction: column; /* Display tabs menu and content vertically */}\n    #tabs {\n        border-bottom: 1px solid black;\n        text-align: center;}\n    .theme {\n        width: 100%;\n        margin-bottom: 20px;}\n    .theme-columns {\n        display: flex;}\n    .select,\n    .theme-description {\n        flex: 1;\n        margin-right: 20px;}\n    #uploader {\n        margin-top: 20px;}\n    #tabs{\n        width: 100%}\n    #config_content {\n        text-align: left;}\n    label {\n        text-align: center;}\n    .ui-image {\n        width: 100%;\n        max-width: 600px; \n        left: 50%;\n        transform: translateX(-50%);\n        position: relative;\n        background-color: black;\n    }\n    .config-box {\n        max-width: 100%;\n        width: 600px;\n        max-height: 300px;\n        resize: both; /* allow resizing in both directions */\n        overflow: auto;\n        padding: 10px;\n        border: 1px solid #ccc;}\n    #fancygotchi {\n        font-size: 10px;\n        text-align: center;\n        white-space: pre; /* Preserve the spaces in ASCII art */\n        font-family: monospace;\n    }\n    #sticky-button {\n        max-width: 150px;\n        position: fixed;\n        bottom: 15px; /* Distance from the bottom of the screen */\n        left: 50%; /* Center the button horizontally */\n        transform: translateX(-50%); /* Adjust the centering */\n        cursor: pointer;\n        z-index: 1000; /* Ensure it stays above other elements */\n    }\n    .preserve-line-breaks {\n        white-space: pre-wrap;\n    }\n    .glitch-line {\n        display: inline-block;\n        position: relative;\n        animation: glitch 0.3s ease-in-out forwards; /* Slower animation duration */\n    }\n    /* Style the button */\n    .scroll-to-top-btn {\n        max-width: 100px;\n        position: fixed;\n        bottom: 00px;\n        right: 40px;\n        z-index: 100; /* Ensure it's on top of other elements */\n        cursor: pointer;\n        display: none; /* Initially hidden */\n        font-size: 24px; /* Make the arrow bigger */\n    }\n    #theNet{\n        display: none;\n        position: absolute;\n        bottom: 0px;\n        font-size: 9px;\n        right: 25px;\n        padding:10px;\n        cursor: context-menu;\n        -webkit-touch-callout: none; /* iOS Safari */\n            -webkit-user-select: none; /* Safari */\n                -khtml-user-select: none; /* Konqueror HTML */\n                    -moz-user-select: none; /* Old versions of Firefox */\n                        -ms-user-select: none; /* Internet Explorer/Edge */\n                            user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */\n    }\n\n    /* Show button when scrolling */\n    .scroll-to-top-btn.show {\n        display: block;\n    }\n    #footer {\n        backkground-color: black;\n        position: fixed;\n        bottom: 0;\n        width: 400px;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        z-index: 1000;\n        text-align: center;\n    }\n    #logo {\n        #left: 50%;\n        #transform: translate(-50%, -50%);\n        margin-right: 40px;\n        margin-left: 40px;\n    }\n    .dev {\n        #display: none;\n    }\n\n    @keyframes glitch {\n        0% {\n            transform: translateX(0);\n        }\n        20% {\n            transform: translateX(-2px); /* Smaller shift */\n        }\n        40% {\n            transform: translateX(2px); /* Smaller shift */\n        }\n        60% {\n            transform: translateX(-1px); /* Smaller shift */\n        }\n        80% {\n            transform: translateX(1px); /* Smaller shift */\n        }\n        100% {\n            transform: translateX(0);\n        }\n    }\n\n    /* Main container with 3 sections */\n    .container {\n        display: flex; /* Horizontal alignment */\n        align-items: flex-start; /* Align items to the top */\n        width: 100%;\n        gap: 0px; /* No gap between containers */\n        max-height: 100%;\n    }\n\n    /* Left section (ensures minimal size) */\n    .left-container {\n        display: block; /* Prevent flex stretching */\n        width: 33.33%;\n        max-width: 100%;\n        min-height: 100%; /* Minimum height is set to 100% of the container */\n        max-height: 100%; /* Maximum height is 100% of the container */\n    }\n\n    /* Grid layout inside the left-container */\n    .left {\n        display: grid;\n        grid-template-columns: repeat(6, minmax(20px, 1fr)); /* Equal column width */\n        grid-template-rows: repeat(6, minmax(35px, auto)); /* Dynamic rows with a minimum height */\n        gap: 0px; /* No spacing between grid items */\n        width: 100%; /* Fit the container width */\n        height: auto; /* Adjusts automatically based on content */\n        min-height: 100%; /* Ensures it doesn’t collapse */\n    }\n\n    /* Flip switch buttons */\n    .left button {\n        display: block; /* Prevent inline layout issues */\n        width: 100%; /* Fit the parent width */\n        max-width: 100%; /* Prevent overflow */\n        padding: 0; /* Remove extra padding */\n        margin: 0; /* Remove extra margin */\n    }\n\n    .left-top {\n        display: grid;\n        grid-template-columns: repeat(3, minmax(0, 1fr));\n        width: 100%; /* Fit the parent width */\n        height: auto; /* Adjusts automatically based on content */\n        min-height: 100%; /* Ensures it doesn’t collapse */\n        gap: 30px;\n        align-items: center;\n        padding-top: 0px;\n        margin-top: 0px;\n        justify-content: center;\n    }\n\n    .left-top ui.flipswitch {\n        justify-self: center;\n        align-self: center;\n        margin: auto;\n        width:100%;\n        position: relative;\n        display: inline-block;\n    }\n\n    .left-top div{\n        max-width: 100%;\n        width: 100%;\n    }\n\n    /* Center element truly centered */\n    .center-container {\n        display: flex;\n        justify-content: center;\n        align-items: center; /* Ensures vertical centering */\n        width: 33.33%;\n        min-width: 400px;\n        min-height: 100%; /* Allow content to expand with the container */\n        max-height: 100%; /* Ensure no stretching beyond the container */\n    }\n\n    .center img {\n        max-width: 100%;\n        height: auto;\n    }\n\n    /* Right section centered within its area */\n    .right-container {\n        display: flex;\n        justify-content: center;\n        align-items: center; /* Ensures vertical centering */\n        width: 33.33%;\n        min-width: 400px;\n        min-height: 100%; /* Allow content to expand with the container */\n        max-height: 100%; /* Ensure no stretching beyond the container */\n    }\n\n    .right img {\n        max-width: 100%;\n        height: auto;\n    }\n\n    /* Responsive stacking for small screens */\n    @media (max-width: 800px) {\n        .container {\n            flex-direction: column;\n            align-items: center;\n        }\n\n        .left-container, .center-container, .right-container {\n            width: 100%; /* Full width on smaller screens */\n            margin-bottom: 10px;\n        }\n    }\n\n    .btn_grey {\n        background-color: #f2f2f2;\n    }\n    .btn_red {\n        background-color: #ff0000;\n    }\n    .btn_green {\n        background-color: #00ff00;\n    }\n    .ui-btn .btn_blue {\n        background-color: #0000ff;\n        color: #0000ff;\n    }\n    .btn_yellow {\n        background-color: #ffff00;\n    }\n    .btn_black {\n        background-color: #000000;\n    }\n    .btn_white {\n        background-color: #ffffff;\n    }\n\n    /* Style individual arrow buttons */\n    .arrow-button {\n        font-weight: bold;\n        font-size: 24px; /* Makes the arrow icon larger */\n    }\n    #download_window {\n        display: none;\n    }\n    #loading-spinner{\n    \n    }\n</style>\n{% endblock %}\n{% block content %}\n    <div id=\"editor\">\n        \n        <div id=\"main\" data-role=\"navbar\">\n            <ul>\n                <li>\n                    <form class=\"action\" method=\"post\" action=\"/shutdown\" onsubmit=\"return confirm('this will halt the unit, continue?');\">\n                        <input type=\"submit\" class=\"button ui-btn ui-corner-all\" value=\"Shutdown\"/>\n                        <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n                    </form>\n                </li>\n                <li>\n                    <form class=\"action\" method=\"post\" action=\"/reboot\" onsubmit=\"return confirm('this will reboot the unit, continue?');\">\n                        <input type=\"submit\" class=\"button ui-btn ui-corner-all\" value=\"Reboot\"/>\n                        <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n                    </form>\n                </li>\n\n                <li>\n                    <form class=\"action\" method=\"post\" action=\"/restart\" onsubmit=\"return confirm('This will restart the service in Manu mode, continue?');\">\n                        <input type=\"submit\" class=\"button ui-btn ui-corner-all\" value=\"Restart in Manu mode\"/>\n                        <input type=\"hidden\" name=\"mode\" value=\"MANU\"/>\n                        <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n                    </form>\n                </li>\n                <li>\n                    <form class=\"action\" method=\"post\" action=\"/restart\" onsubmit=\"return confirm('This will restart the service in Auto mode, continue?');\">\n                        <input type=\"submit\" class=\"button ui-btn ui-corner-all\" value=\"Restart in Auto mode\"/>\n                        <input type=\"hidden\" name=\"mode\" value=\"AUTO\"/>\n                        <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\"/>\n                    </form>\n                </li>\n            </ul>\n        </div>\n        <div id=\"display\" data-role=\"navbar\" class=\"dev\">\n            <ul>\n                <li>\n                    <button id=\"display_hijack\" onclick=\"display_hijack()\">Second hardware display</button>\n                </li>\n                <li>\n                    <button id=\"display_pwny\" onclick=\"display_pwny()\">Pwnagotchi hardware display</button>\n                </li>\n            </ul>\n            <ul>\n                <li>\n                    <button id=\"display_next\" onclick=\"display_next()\">Next second screen mode</button>\n                </li>\n                <li>\n                    <button id=\"display_previous\" onclick=\"display_previous()\">Previous second screen mode</button>\n                </li>\n                <li>\n                    <button id=\"screen_saver_next\" onclick=\"screen_saver_next()\">Next screen saver</button>\n                </li>\n                <li>\n                    <button id=\"screen_saver_previous\" onclick=\"screen_saver_previous()\">Previous screen saver</button>\n                </li>\n            </ul>\n        </div>\n        <div class=\"container\">\n            <!-- Left Div with 6x5 grid -->\n            <div class=\"left-container\">\n                <div class=\"left-top\">\n                    <div>\n                        <label for=\"screen\">Screen</label>\n                        <input type=\"checkbox\"data-role=\"flipswitch\" name=\"Screen 2\" id=\"screen\" data-on-text=\"Screen 2\" data-off-text=\"Screen 1\" data-wrapper-class=\"custom-size-flipswitch\">\n                    </div>\n                    <div><button id=\"stealth\" onclick=\"stealth()\">Stealth</button></div>\n                    <div>\n                        <label for=\"keyboard\">Keyboard</label>\n                        <input type=\"checkbox\"data-role=\"flipswitch\" name=\"Enabled\" id=\"keyboard\" data-on-text=\"Enabled\" data-off-text=\"Disabled\" data-wrapper-class=\"custom-size-flipswitch\">\n                    </div>\n                    \n                </div>\n                <div class=\"left\">\n                    <div><button style=\"background-color: #a3a5a4; color: #222;\" id=\"l2\" class=\"btn_grey\" onclick=\"navigate('l2')\">L2</button></div>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                    <div><button style=\"background-color: #a3a5a4; color: #222;\" id=\"r2\" onclick=\"navigate('r2')\">R2</button></div>\n                    \n                    <div><button style=\"background-color: #a3a5a4; color: #222;\" id=\"l1\" onclick=\"navigate('l1')\">L1</button></div>\n                    <div><button style=\"background-color: #515151; color: #222;\" id=\"up\" onclick=\"navigate('up')\" class=\"arrow-button\">↑</button></div>\n                    <div></div>\n                    <div></div>\n                    <div><button style=\"background-color: #0749b4; color: #000077;\" id=\"x\" onclick=\"navigate('x')\" class=\"arrow-button\">X</button></div>\n                    <div><button style=\"background-color: #a3a5a4; color: #222;\" id=\"r1\" onclick=\"navigate('r1')\">R1</button></div>\n                    \n                    <div><button style=\"background-color: #515151; color: #222;\" id=\"left\" onclick=\"navigate('left')\" class=\"arrow-button\">←</button></div>\n                    <div><button style=\"background-color: #515151; color: #222;\"></div>\n                    <div><button style=\"background-color: #515151; color: #222;\" id=\"right\" onclick=\"navigate('right')\" class=\"arrow-button\">→</button></div>\n                    <div><button style=\"background-color: #008d45; color: #007700;\" id=\"y\" onclick=\"navigate('y')\" class=\"arrow-button\">Y</button></div>\n                    <div></div>\n                    <div><button style=\"background-color: #eb1a1d; color: #770000;\" id=\"a\" onclick=\"navigate('a')\" class=\"arrow-button\">A</button></div>\n\n                    <div></div>\n                    <div><button style=\"background-color: #515151; color: #222;\" id=\"down\" onclick=\"navigate('down')\" class=\"arrow-button\">↓</button></div>\n                    <div><button style=\"background-color: #4e5955; color: #000;\" id=\"select\" onclick=\"navigate('select')\">Select</button></div>\n                    <div><button style=\"background-color: #4e5955; color: #000;\" id=\"start\" class=\"btn_grey\" onclick=\"navigate('start')\">Start</button></div>\n                    <div><button style=\"background-color: #fece15; color: #777700;\" id=\"b\" onclick=\"navigate('b')\" class=\"arrow-button\">B</button></div>\n                    <div></div>\n\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                </div>\n            </div>\n            \n\n            <!-- Center Image -->\n            <div class=\"center-container\">\n                <div class=\"center\">\n                    <img class=\"ui-image pixelated\" src=\"/ui\" id=\"ui\" style=\"width: 400px\" />\n                </div>\n            </div>\n\n            <!-- Right Image -->\n            <div class=\"right-container\">\n                <div class=\"right\">\n                    <img class=\"ui-image pixelated\" src=\"/plugins/Fancygotchi/ui2\" id=\"ui2\" style=\"width: 400px\" />\n                </div>\n            </div>\n        </div>\n\n        <div id=\"wrap\" data-role=\"tabs\">\n            <div id=\"tabs\" data-role=\"navbar\">\n                <ul>\n                    <li class=\"ui-btn-active\"><a href=\"#theme\" data-theme=\"a\" data-ajax=\"false\">Theme manager</a></li>\n                    <li class=\"\"><a href=\"#theme_downloader\" data-theme=\"a\" data-ajax=\"false\">Theme downloader</a></li>\n                    <li class=\"\"><a href=\"#config\" data-theme=\"a\" data-ajax=\"false\">Configuration</a></li>\n                    <li class=\"\"><a href=\"#theme_editor\" data-theme=\"a\" data-ajax=\"false\">Theme editor</a></li>\n                </ul>\n            </div>\n            <div id=\"theme\" class=\"ui-content theme\">\n                <div id=\"theme-columns\" class=\"row theme-columns\">\n                    <div id=\"select\" class=\"column select\">\n                        <label for=\"theme-selector\">Select a theme:</label>\n                        <select id=\"theme-selector\">\n                            <option value=\"Default\"{% if default_theme == '' %}selected{% endif %}>Default</option>\n                            {% for theme in themes %}\n                            <option value=\"{{ theme }}\"{% if default_theme == theme %}selected{% endif %}>{{ theme }}</option>\n                            {% endfor %}\n                        </select>\n                        <br>\n                        <label for=\"orientation-selector\">Select an orientation:</label>\n                        <select id=\"orientation-selector\">\n                            <option value=0{% if rotation == 0 %} selected{% endif %}>0</option>\n                            <option value=90{% if rotation == 90 %} selected{% endif %}>90</option>\n                            <option value=180{% if rotation == 180 %} selected{% endif %}>180</option>\n                            <option value=270{% if rotation == 270 %} selected{% endif %}>270</option>\n                        </select>\n                        <button id=\"select-theme-button\" onclick=\"theme_select()\">Select Theme</button>\n                        <button id=\"copy-theme-button\" onclick=\"copyTheme()\">Copy Theme</button>\n                        <button id=\"rename-theme-button\" onclick=\"renameTheme()\">Rename Theme</button>\n\n                        <button id=\"select-theme-button\" onclick=\"theme_delete()\">Delete Theme</button>\n                        <button id=\"export-theme-button\" onclick=\"theme_export()\">Export Theme</button>\n                        <div id=\"uploader\" class=\"ui-content\">\n                            <form id=\"uploadForm\" enctype=\"multipart/form-data\">\n                                <input type=\"file\" name=\"zipFile\" id=\"zipFile\">\n                                <input type=\"submit\" value=\"Upload Theme Zip\" onclick=\"theme_upload(event)\">\n                            </form>\n                            <div id=\"message\"></div>\n                        </div>\n                        <div id=\"create-theme\">\n                            <h3>Create New Theme</h3>\n                            <input type=\"text\" id=\"new-theme-name\" placeholder=\"Enter new theme name\">\n                            <label><input type=\"checkbox\" id=\"use-resolution\"> Use Resolution System</label>\n                            <label><input type=\"checkbox\" id=\"use-orientation\"> Use Orientation System</label>\n                            <button id=\"create-theme-button\" onclick=\"createNewTheme()\">Create Theme</button>\n                        </div>\n                    </div>\n                    <div id=\"theme-description\" class=\"column theme-description\">\n                        <h3>Theme Description</h3>\n                        <div id=\"theme-description-content\"></div>\n                        <img id=\"screenshot\" src=\"/img/screenshot.png\" onerror=\"this.onerror=null; this.src='/screenshots/screenshot.png';\"></img>\n                    </div>\n                </div>\n            </div>\n            <div id=\"theme_downloader\" class=\"ui-content theme\">\n                <div id=\"download_list_refresh\">\n                    <p align=\"center\">\n                        <button id=\"select-theme-downloader-btn\" onclick=\"loadThemeRepo()\">Load theme list</button>\n                    </p>\n                </div>\n                <div id=\"loading-spinner\" style=\"display:none;\"><p align=\"center\">Loading...</p></div>\n                <div id=\"download_window\" style=\"display:none;\">\n                    <div id=\"theme-downloader-columns\" class=\"row theme-columns\">\n                        <div id=\"downloader-select\" class=\"column select\">\n                            <label for=\"theme-downloader-selector\">Select a theme:</label>\n                            <select id=\"theme-downloader-selector\">\n                                <!-- Themes will be dynamically populated here -->\n                            </select>\n                            <br>\n                            <button id=\"select-theme-downloader-button\" onclick=\"theme_download_select()\">Select Theme</button>\n                        </div>\n                        <div id=\"theme-downloader-description\" class=\"column theme-description\">\n                            <h3>Theme Description</h3>\n                            <div id=\"theme-downloader-description-content\"><p>No description available</p></div>\n                            <img id=\"repo_screenshot\" src=\"/screenshots/screenshot.png\" onerror=\"this.onerror=null; this.src='/screenshots/screenshot.png';\"></img>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <div id=\"config\" class=\"ui-content\">\n                <h2>No configuration for the default theme</h2> <!-- Updated dynamically -->\n                <div id=\"hidden\">\n                    <button onclick=\"saveConfig()\" id=\"sticky-button\">Save Configuration</button>\n                    <h3>Configuration editor</h3>\n                    <input type=\"text\" id=\"configSearch\" onkeyup=\"searchConfig()\" placeholder=\"Search for options...\" title=\"Type in a name\">\n                    <h4>Config Path</h4> <!-- Updated dynamically -->\n                    <div id=\"config_content\"></div> <!-- Config data inserted here dynamically -->\n                    <h3>CSS editor</h3>\n                    <h4>CSS Path</h4>  <!-- css path inserted here dynamically -->\n                    <div contenteditable=\"true\" id=\"CSS\" class=\"config-box\"></div> <!-- css content inserted here dynamically -->\n                    <h3>Info editor</h3>\n                    <h4>Info Path</h4>  <!-- css path inserted here dynamically -->\n                    <div contenteditable=\"true\" id=\"Info\" class=\"config-box\"></div> <!-- Info content inserted here dynamically -->\n                </div>\n                <button onclick=\"resetCSS()\">Reset Pwnagotchi core CSS</button>\n            </div>\n\n            <div id=\"theme_editor\" class=\"ui-content\">\n                \n                <div id=\"fancygotchi\">\n                    <h2>Theme editor</h2>\n                    <div id=\"theme_editor_content\">\n                        <h2>Coming soon !</h2>\n                        <h2>If you like the project feel free to contribute !</h2>\n                        <h2><a href='{{fancy_repo}}'>Fancygotchi</a> is made with ❤ by <a href='https://linktr.ee/v0r_t3x'>V0rT3x</a></h2>\n                    </div>\n                    <div id=\"logo\">\n                        <pre>\n{% for line in logo.splitlines() %}<span>{{ line }}</span>\n{% endfor %}\n                        </pre>\n                    </div>\n                </div>\n                <div id=\"theNet\"><a onclick=\"theNet()\">π</a></div>\n            </div>\n        </div>\n        \n        <div id=\"footer\">\n            <a href='{{fancy_repo}}'>Fancygotchi</a> {{ version }} made with ❤ by <a href='https://linktr.ee/v0r_t3x'>{{ author }}</a>\n        </div>\n        \n    </div>\n    <div data-role=\"popup\" id=\"delete-dialog\" data-overlay-theme=\"b\" data-theme=\"b\" data-dismissible=\"false\" style=\"max-width:400px;\">\n        <div role=\"main\" class=\"ui-content\">\n            <h3 class=\"ui-title\">Confirm Deletion</h3>\n            <p>Are you sure you want to delete the selected theme?</p>\n            <a href=\"#\" class=\"ui-btn ui-corner-all ui-shadow ui-btn-inline ui-btn-b\" data-rel=\"back\">Cancel</a>\n            <a href=\"#\" id=\"confirm-delete\" class=\"ui-btn ui-corner-all ui-shadow ui-btn-inline ui-btn-b\" data-rel=\"back\">Delete</a>\n        </div>\n    </div>\n    <button id=\"scrollToTopBtn\" class=\"scroll-to-top-btn\">&#x25B2;</button>\n{% endblock %}\n{% block script %}\ntheme_info(\"{{name}}\");\nloadConfig(0, \"{{name}}\");\n\nvar scrollToTopBtn = document.getElementById(\"scrollToTopBtn\");\n\nfunction theNet() {\n    var div = document.querySelector(\".dev\");\n    var logo = document.querySelector(\"#logo\");\n\n    if (!div || !logo) {\n        console.error('Element not found: .dev or #logo');\n        return;\n    }\n\n    var computedColor = window.getComputedStyle(logo).color;\n    console.log(computedColor)\n\n    function rgbToColor(rgb) {\n        return rgb.replace(/\\s+/g, '').toLowerCase();\n    }\n\n    var limeColor = rgbToColor(\"rgb(0, 255, 0)\"); \n\n    if (div.style.display === \"none\" || div.style.display === \"\") {\n        if (rgbToColor(computedColor) === limeColor) {\n            logo.style.color = \"red\"; \n        } else {\n            logo.style.color = \"lime\"; \n        }\n\n        glitchEffect(true);\n        div.style.display = \"block\";\n        logo.style.backgroundColor = \"black\";\n    } else {\n        div.style.display = \"none\";\n        logo.style.color = \"\"; \n        logo.style.backgroundColor = \"\";\n    }\n}\n\nwindow.onload = function() {\n    var image = document.getElementById(\"ui\");\n    var image2 = document.getElementById(\"ui2\");\n    function updateImage() {\n        image.src = image.src.split(\"?\")[0] + \"?\" + new Date().getTime();\n        image2.src = image2.src.split(\"?\")[0] + \"?\" + new Date().getTime();\n    }\n    setInterval(updateImage, {{webui_fps}});\n}\n\n$(document).ready(function () {\n    // Variable to track the keyboard toggle state\n    let keyboardActive = false;\n\n    // Listen for changes on the keyboard toggle flipswitch\n    $('#keyboard').on('change', function () {\n        keyboardActive = $(this).is(':checked'); // Set true if checked, false otherwise\n    });\n\n    // Keydown event listener\n    $(document).on('keydown', function (e) {\n        if (!keyboardActive) return; // Exit if keyboard is toggled off\n        switch (e.key) {\n            case \"ArrowUp\":\n                e.preventDefault(); \n                $('#up').click();\n                break;\n            case \"ArrowDown\":\n                e.preventDefault(); \n                $('#down').click();\n                break;\n            case \"ArrowLeft\":\n                e.preventDefault(); \n                $('#left').click();\n                break;\n            case \"ArrowRight\":\n                e.preventDefault(); \n                $('#right').click();\n                break;\n            case \"Enter\":\n                e.preventDefault(); \n                $('#select').click();\n                break;\n            case \"s\":\n                $('#stealth').click();\n                break;\n            case \"t\":\n                $('#start').click();\n                break;\n            case \"a\":\n                $('#a').click();\n                break;\n            case \"b\":\n                $('#b').click();\n                break;\n            case \"x\":\n                $('#x').click();\n                break;\n            case \"y\":\n                $('#y').click();\n                break;\n            case \"Shift\":\n                e.preventDefault(); \n                $('#l1').click();\n                break;\n            case \"Alt\":\n                e.preventDefault(); \n                $('#r1').click();\n                break;\n            case \"Control\":\n                e.preventDefault(); \n                $('#l2').click();\n                break;\n            case \" \":\n                e.preventDefault(); \n                $('#r2').click();\n                break;\n        }\n    });\n});\n\nwindow.onscroll = function() {\n    if (document.body.scrollTop > 100 || document.documentElement.scrollTop > 100) {\n        scrollToTopBtn.classList.add(\"show\");\n    } else {\n        scrollToTopBtn.classList.remove(\"show\");\n    }\n};\n\nscrollToTopBtn.addEventListener(\"click\", function() {\n    window.scrollTo({top: 0, behavior: 'smooth'});\n});\n\nfunction searchConfig() {\n    var input, filter, table, tr, i, td, txtValue;\n    input = document.getElementById(\"configSearch\");\n    filter = input.value.toUpperCase();\n    table = document.getElementById(\"tableOptions\");\n    if (!table) return;\n    tr = table.getElementsByTagName(\"tr\");\n\n    // Loop through all table rows (except the header and the 'add' row), and hide those who don't match the search query\n    for (i = 1; i < tr.length -1; i++) {\n        td = tr[i].getElementsByTagName(\"td\")[1]; // The second column contains the option name\n        if (td) {\n            txtValue = td.textContent || td.innerText;\n            if (txtValue.toUpperCase().indexOf(filter) > -1) {\n                tr[i].style.display = \"\";\n            } else {\n                tr[i].style.display = \"none\";\n            }\n        }\n    }\n}\nfunction active_theme(callback) {\n    loadJSON(\"Fancygotchi/active_theme\", function(response) {\n        callback(response.theme);\n    });\n}\n\nfunction resetCSS() {\n    loadJSON(\"Fancygotchi/reset_css\", function(response) {\n        console.log(\"CSS reset successful!\");\n        alert(\"CSS reset successful!\");\n    });\n}\n\nfunction theme_select() {\n    var theme = document.getElementById(\"theme-selector\").value;\n    var rotation = document.getElementById(\"orientation-selector\").value;\n    \n    //var json = {\"theme\": theme, \"rotation\": rotation};\n    var url = \"Fancygotchi/theme_select?theme=\"+theme+\"&rotation=\"+rotation;\n    console.log(url);\n    loadJSON(url, function(response) {\n        loadConfig(1, theme);\n    }); \n}\n\nfunction loadConfig(a, theme) {\n    if (a == 1) {\n        alert(theme + ' selected');\n    }\n    if (theme == \"Default\") {\n        document.querySelector(\"#config h2\").innerText = \"No configuration for the default theme\";\n        document.getElementById(\"hidden\").style.visibility = \"hidden\";\n        document.getElementById(\"hidden\").style.display = \"none\";\n    } else {\n        document.getElementById(\"hidden\").style.visibility = \"visible\";\n        document.getElementById(\"hidden\").style.display = \"inline-block\";\n    }\n    loadJSON(\"Fancygotchi/load_config\", function(response) {\n        updateConfigSection(response);\n    });\n}\n\nfunction escapeHtml(text) {\n    return text\n        .replace(/</g, \"&lt;\")\n        .replace(/>/g, \"&gt;\");\n}\n\nfunction updateConfigSection(data) {\n    populateConfig(data.config)\n    if (data.name == \"Default\"  || data.name == \"\") {\n        document.querySelector(\"#config h2\").innerText = \"No configuration for the default theme\";\n\n    } else {\n        document.querySelector(\"#config h2\").innerText = \"Configuration of \" + data.name;\n\n    }\n    document.querySelector(\"#config h4:nth-of-type(1)\").innerText = data.cfg_path;\n    document.querySelector(\"#config h4:nth-of-type(2)\").innerText = data.css_path;\n    var cssContent = document.getElementById(\"CSS\");\n    cssContent.innerHTML = '<div class=\"preserve-line-breaks\">' + escapeHtml(data.css) + '</div>';\n    document.querySelector(\"#config h4:nth-of-type(3)\").innerText = data.info_path;\n    var infoContent = document.getElementById(\"Info\");\n    infoContent.innerHTML = '<div class=\"preserve-line-breaks\">' + escapeHtml(data.info) + '</div>';\n}\n\nfunction populateConfig(config) {\n    var configContent = $('#config_content');\n    configContent.empty();\n    var table = jsonToTable(flattenJson(config));\n    configContent.append(table);\n}\n\nfunction jsonToTable(json) {\n  var table = document.createElement(\"table\");\n  table.id = \"tableOptions\";\n\n  var tr = table.insertRow();\n  var thDel = document.createElement(\"th\");\n  thDel.innerHTML = \"\";\n  var thOpt = document.createElement(\"th\");\n  thOpt.innerHTML = \"Option\";\n  var thVal = document.createElement(\"th\");\n  thVal.innerHTML = \"Value\";\n  tr.appendChild(thDel);\n  tr.appendChild(thOpt);\n  tr.appendChild(thVal);\n\n  var td, divDelBtn, btnDel;\n  Object.keys(json).forEach(function(key) {\n    tr = table.insertRow();\n    divDelBtn = document.createElement(\"div\");\n    divDelBtn.className = \"del_btn_wrapper\";\n    td = document.createElement(\"td\");\n    td.setAttribute(\"data-label\", \"\");\n    if (!key.startsWith(\"theme.options\")) {\n      btnDel = document.createElement(\"Button\");\n      btnDel.innerHTML = \"X\";\n      btnDel.setAttribute(\"data-key\", key);\n      btnDel.onclick = function(){ delRow(this);};\n      btnDel.className = \"remove\";\n      divDelBtn.appendChild(btnDel);\n      td.appendChild(divDelBtn);\n    }\n    tr.appendChild(td);\n    td = document.createElement(\"td\");\n    td.setAttribute(\"data-label\", \"Option\");\n    td.innerHTML = key;\n    tr.appendChild(td);\n    td = document.createElement(\"td\");\n    td.setAttribute(\"data-label\", \"Value\");\n    if(typeof(json[key])==='boolean'){\n      var input = document.createElement(\"select\");\n      input.setAttribute(\"id\", \"boolSelect\");\n      var tvalue = document.createElement(\"option\");\n      tvalue.setAttribute(\"value\", \"true\");\n      var ttext = document.createTextNode(\"True\")\n      tvalue.appendChild(ttext);\n      var fvalue = document.createElement(\"option\");\n      fvalue.setAttribute(\"value\", \"false\");\n      var ftext = document.createTextNode(\"False\");\n      fvalue.appendChild(ftext);\n      input.appendChild(tvalue);\n      input.appendChild(fvalue);\n      input.value = json[key];\n      td.appendChild(input);\n    } else {\n      var input = document.createElement(\"input\");\n      if(Array.isArray(json[key])) {\n        input.type = 'text';\n        input.value = '[' + json[key].join(', ') + ']';\n      } else {\n        input.type = typeof(json[key]);\n        input.value = json[key];\n      }\n      td.appendChild(input);\n    }\n    tr.appendChild(td);\n  });\n\n  var newTr = table.insertRow();\n  var newTd = newTr.insertCell();\n  newTd.setAttribute(\"data-label\", \"\");\n  var addButton = document.createElement(\"button\");\n  addButton.innerHTML = \"+\";\n  addButton.onclick = function() {\n    var newRow = table.insertRow();\n    var newTd = newRow.insertCell();\n    var delButton = document.createElement(\"button\");\n    delButton.innerHTML = \"X\";\n    delButton.onclick = function() {\n      this.parentNode.parentNode.remove();\n    };\n    newTd.appendChild(delButton);\n    var newKeyCell = newRow.insertCell();\n    var newKeyInput = document.createElement(\"input\");\n    newKeyInput.type = \"text\";\n    newKeyInput.placeholder = \"New Key\";\n    newKeyCell.appendChild(newKeyInput);\n\n    var newValueCell = newRow.insertCell();\n    var newValueInput = document.createElement(\"input\");\n    newValueInput.type = \"text\";\n    newValueInput.placeholder = \"New Value\";\n    newValueCell.appendChild(newValueInput);\n  };\n  newTd.appendChild(addButton);\n  newTr.appendChild(newTd);\n  newTr.appendChild(document.createElement(\"td\"));\n\n  return table;\n}\nfunction delRow(btn) {\n    var key = btn.getAttribute(\"data-key\");\n    var tr = btn.closest(\"tr\");\n    if (tr && key) {\n        tr.parentNode.removeChild(tr);\n    }\n}\n\nfunction saveConfig() {\n    var config = document.getElementById(\"tableOptions\");\n    var css = document.getElementById(\"CSS\").textContent;\n    var info = document.getElementById(\"Info\").textContent;\n\n    console.log(info)\n    console.log(css)\n\n    var data = {\n        config: tableToJson(config),\n        css: css,\n        info: info\n    };\n    sendJSON(\"Fancygotchi/save_config\", data, function(response) {\n        if (response.status == \"200\") {\n            alert(\"Config got updated\");\n        } else {\n            alert(\"Error while updating the config (err-code: \" + response.status + \")\");\n        }\n    });\n    active_theme(function(activeTheme) {\n        loadConfig(0, activeTheme)\n        theme_info(activeTheme)\n    });\n}\n\nfunction tableToJson(table) {\n  var rows = table.getElementsByTagName(\"tr\");\n  var i, td, key, value;\n  var json = {};\n\n  for (i = 0; i < rows.length; i++) {\n    td = rows[i].getElementsByTagName(\"td\");\n    if (td.length == 3) {\n      key = td[1].textContent || td[1].innerText;\n      console.log(td[1].textContent || td[1].innerText);\n      var input = td[2].getElementsByTagName(\"input\");\n      var select = td[2].getElementsByTagName(\"select\");\n      console.log(key);\n\n      if (input && input.length > 0) {\n        if (input[0].type == \"text\") {\n          const inputValue = input[0].value.trim();\n          if (inputValue === \"\") {\n            value = \"\"; \n          } else if (inputValue.startsWith(\"[\") && inputValue.endsWith(\"]\")) {\n            try {\n              value = JSON.parse(inputValue); \n            } catch (e) {\n              console.error('Invalid JSON array:', inputValue);\n              value = inputValue;\n            }\n          } else if (inputValue === 'true' || inputValue === 'false') {\n            value = inputValue === 'true'; \n          } else if (!isNaN(inputValue)) {\n            value = parseInt(inputValue, 10); \n          } else {\n            value = inputValue;\n          }\n        } else if (input[0].type == \"number\") {\n          value = Number(input[0].value); \n        }\n      } else if (select && select.length > 0) {\n        value = select[0].options[select[0].selectedIndex].value === 'true';\n      }\n\n      var keyParts = key.split('.');\n      var currentObj = json;\n      for (var j = 0; j < keyParts.length - 1; j++) {\n        if (!currentObj[keyParts[j]]) {\n          currentObj[keyParts[j]] = {}; \n        }\n        currentObj = currentObj[keyParts[j]];\n      }\n      currentObj[keyParts[keyParts.length - 1]] = value;\n    }\n  }\n\n  var newRows = document.querySelectorAll(\"tr input[type='text'][placeholder='New Key']\");\n  newRows.forEach(function(newKeyInput) {\n    var newValueInput = newKeyInput.closest(\"tr\").querySelector(\"input[placeholder='New Value']\");\n    var newKey = newKeyInput.value.trim();\n    var newValue = newValueInput.value.trim();\n\n    \n  if (newKey) {\n    if (newValue === \"\") {\n      newValue = \"\";\n    } else if (newValue.startsWith(\"[\") && newValue.endsWith(\"]\")) {\n      try {\n        newValue = JSON.parse(newValue);\n      } catch (e) {\n        console.error('Invalid JSON array:', newValue);\n        newValue = newValue;\n      }\n    } else if (newValue === 'true' || newValue === 'false') {\n      newValue = newValue === 'true';\n    } else if (!isNaN(newValue)) {\n      newValue = parseFloat(newValue);\n    } else {\n      newValue = newValue;\n    }\n\n      var newKeyParts = newKey.split('.');\n      var currentNewObj = json;\n        console.log(newKeyParts)\n      for (var k = 0; k < newKeyParts.length - 1; k++) {\n        if (!currentNewObj[newKeyParts[k]]) {\n          currentNewObj[newKeyParts[k]] = {};\n        }\n        currentNewObj = currentNewObj[newKeyParts[k]];\n      }\n      currentNewObj[newKeyParts[newKeyParts.length - 1]] = newValue;\n    }\n  });\n\n  return unFlattenJson(json);\n}\n\nfunction unFlattenJson(data) {\n    \"use strict\";\n    if (Object(data) !== data || Array.isArray(data))\n        return data;\n    var result = {}, cur, prop, idx, last, temp, inarray;\n    for(var p in data) {\n        cur = result, prop = \"\", last = 0, inarray = false;\n        do {\n            idx = p.indexOf(\".\", last);\n            temp = p.substring(last, idx !== -1 ? idx : undefined);\n            inarray = temp.startsWith('#') && !isNaN(parseInt(temp.substring(1)))\n            cur = cur[prop] || (cur[prop] = (inarray ? [] : {}));\n            if (inarray){\n                prop = temp.substring(1);\n            }else{\n                prop = temp;\n            }\n            last = idx + 1;\n        } while(idx >= 0);\n        cur[prop] = data[p];\n    }\n    return result[\"\"];\n}\n\nfunction createNewTheme() {\n    var themeName = document.getElementById(\"new-theme-name\").value;\n    var useResolution = document.getElementById(\"use-resolution\").checked;\n    var useOrientation = document.getElementById(\"use-orientation\").checked;\n    if (!themeName) {\n        alert(\"Please enter a theme name\");\n        return;\n    }\n    var json = {\n        \"theme_name\": themeName,\n        \"use_resolution\": useResolution,\n        \"use_orientation\": useOrientation\n    };\n    sendJSON(\"Fancygotchi/create_theme\", json, function(response) {\n        if (response.status == 200) {\n            alert(\"Theme created successfully\");\n            theme_list();\n        } else {\n            alert(\"Error creating theme: \" + response.responseText);\n        }\n    });\n}\n\nfunction copyTheme() {\n    var theme = document.getElementById(\"theme-selector\").value;\n    if (theme != \"Default\") {\n        if (theme) {\n            var newName = theme + '-copy';\n            sendJSON(\"Fancygotchi/theme_copy\", {\"theme\": theme, \"new_name\": newName}, function(response) {\n                if (response.status == 200) {\n                    alert(\"Theme copied successfully\");\n                    theme_list();\n                } else {\n                    alert(\"Error copying theme: \" + response.responseText);\n                }\n            });\n        } else {\n            alert('Please select a theme to copy.');\n        }\n    } else {\n        alert('Default theme cannot be copied.');\n    }\n}\n\nfunction renameTheme() {\n    var theme = document.getElementById(\"theme-selector\").value;\n\n    active_theme(function(activeTheme) {\n        if (theme !== \"Default\" && theme !== activeTheme) {\n            if (theme) {\n                var newName = prompt(\"Enter new name for the theme:\", theme);\n                if (newName && newName !== theme) {\n                    sendJSON(\"Fancygotchi/theme_rename\", {\"theme\": theme, \"new_name\": newName}, function(response) {\n                        if (response.status == 200) {\n                            alert(\"Theme renamed successfully\");\n                            theme_list(); \n                        } else {\n                            alert(\"Error renaming theme: \" + response.responseText);\n                        }\n                    });\n                }\n            } else {\n                alert('Please select a theme to rename.');\n            }\n        } else {\n            alert('Default theme or active theme cannot be renamed.');\n        }\n    });\n}\n\nfunction theme_upload(event) {\n    event.preventDefault();\n\n    var formData = new FormData();\n    var fileInput = document.getElementById('zipFile');\n    var file = fileInput.files[0];\n\n    if (!file) {\n        alert('No file selected.');\n        return;\n    }\n\n    formData.append('zipFile', file);\n\n    sendFormData('Fancygotchi/theme_upload', formData, function(err, response) {\n        if (err) {\n            console.error(err);\n            alert('An error occurred while uploading the theme.');\n            return;\n        }\n\n        if (response.startsWith('Zip file uploaded')) {\n            alert(response);\n            theme_list();\n        } else if (response.startsWith('Some folders were not copied')) {\n            alert(response);\n        } else {\n            alert('Error: ' + response);\n        }\n    });\n}\n\nfunction theme_export() {\n    var selectedTheme = document.getElementById('theme-selector').value;\n    if (selectedTheme != \"Default\") {\n        if (selectedTheme) {\n            window.location.href = 'Fancygotchi/theme_export/' + selectedTheme;\n        } else {\n            alert('Please select a theme to export.');\n        }\n    } else {\n        alert('Default theme cannot be exported.');\n    }\n}\n$(document).on('click', '#confirm-delete', function() {\n    var theme = $('#theme-selector').val();\n    \n    if (theme != \"Default\") {\n        var json = { \"theme\": theme };\n        \n        sendJSON(\"Fancygotchi/theme_delete\", json, function(xhr) {\n            if (xhr.status == 200) {\n                theme_list();\n            }\n        });\n    } else {\n        alert('Default theme cannot be deleted.');\n    }\n});\n\nfunction theme_list() {\n\n    active_theme(function(activeTheme) {\n        loadJSON(\"Fancygotchi/theme_list\", function(response) {\n            populateThemeSelector(response, activeTheme);\n        });\n        $('#theme-selector').val(activeTheme);\n        theme_info(activeTheme);\n    });\n}\nfunction theme_info(activeTheme) {\n    var theme = $('#theme-selector').val();\n    var json = { \"theme\": theme };\n    sendJSON(\"Fancygotchi/theme_info\", json, function(xhr) {\n        if (xhr.status == 200) {\n            var themeInfo = JSON.parse(xhr.responseText);\n            console.log(themeInfo);\n            populateThemeInfo(themeInfo);\n        }\n    });\n}\n$('#theme-selector').change(function() {\n    theme_info($(this).val());\n});\nfunction populateThemeSelector(themes, activeTheme) {\n    var selectElement = $('#theme-selector');\n    selectElement.empty();\n    \n\n    var defaultOption = $('<option>').val('Default').text('Default');\n    selectElement.append(defaultOption);\n    \n    themes.forEach(function(theme) {\n        var option = $('<option>').val(theme).text(theme);\n        if (theme === activeTheme) {\n            option.attr('selected', 'selected');\n        }\n        selectElement.append(option);\n    });\n    \n    if (!themes.includes('Default') && !activeTheme) {\n        defaultOption.attr('selected', 'selected');\n    }\n    \n    selectElement.selectmenu('refresh');\n    return activeTheme\n\n}\nfunction populateThemeInfo(themeInfo) {\n    var $themeDescriptionContent = $('#theme-description-content');\n\n    active_theme(function(activeTheme) {\n        var theme = $('#theme-selector').val() || activeTheme || 'Default';\n        console.log(theme);\n\n        $themeDescriptionContent.empty();\n        $themeDescriptionContent.append('<h3>' + theme.toUpperCase() + '</h3>');\n\n        var $screenshot = $('#screenshot');\n        var screenshotSrc = $('#theme-selector').val() == activeTheme \n            ? '/img/screenshot.png' \n            : '/screenshots/' + theme + '/screenshot.png';\n\n        // Add a cache-busting timestamp parameter\n        $screenshot.attr('src', screenshotSrc + '?cache_buster=' + new Date().getTime());\n        \n        // Log the src attribute to check the updated URL\n        console.log($screenshot.attr('src'));\n\n        $screenshot.on('error', function() {\n            $(this).attr('src', '/screenshots/screenshot.png?cache_buster=' + new Date().getTime());\n        });\n\n        Object.entries(themeInfo).forEach(([key, value]) => {\n            var val = '<span class=\"preserve-line-breaks\">' + value + '</span>';\n            $themeDescriptionContent.append($('<li>').html(key + ': ' + val));\n        });\n    });\n}\n\n\n\nfunction loadThemeRepo() {\n    $('#theme_downloader').find('select, button').prop('disabled', true);\n    $('#loading-spinner').show();\n    $('#download_window').hide();\n    $('#loading-spinner p').text(\"Loading...\");\n\n    loadJSON(\"Fancygotchi/theme_download_list\", function(response) {\n        console.log(response.status);\n        if (response.status == 200) {\n            populateThemeSelector_downloader(response.data);\n            $('#loading-spinner').hide();\n            $('#download_window').show();\n            $('#theme_downloader').find('select, button').prop('disabled', false);\n        }\n        else {\n            var error = response.error || \"An error occurred\";\n            $('#loading-spinner p').text(error);\n            $('#theme_downloader').find('select, button').prop('disabled', false);\n        }\n    });\n}\n\n$('#theme-downloader-selector').change(function() {\n    var themes = window.themes; \n    var selectedTheme = $('#theme-downloader-selector').val();\n    populateThemeInfo_downloader(themes[selectedTheme]);\n});\n\nfunction populateThemeSelector_downloader(themes) {\n    window.themes = themes; \n    var selectElement = $('#theme-downloader-selector');\n    selectElement.empty();\n    const sortedThemes = Object.keys(themes).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n    sortedThemes.forEach(function(theme) {\n        var option = $('<option>').val(theme).text(theme);\n        selectElement.append(option);\n    });\n    selectElement.selectmenu('refresh');\n    if (sortedThemes.length > 0) {\n        populateThemeInfo_downloader(themes[sortedThemes[0]]);\n    }\n}\n\nfunction populateThemeInfo_downloader(themeInfo) {\n    var $themeDescriptionContent = $('#theme-downloader-description-content');\n    var theme = $('#theme-downloader-selector').val();\n    if (theme == '') {\n        theme = 'Default';\n    }\n    $themeDescriptionContent.empty();\n    $themeDescriptionContent.append('<h3>' + theme.toUpperCase() + '</h3>');\n    var img = new Image();\n    var imgPath = '/repo_screenshots/' + theme + '/screenshot.png';\n    img.onload = function() {\n        document.getElementById('repo_screenshot').src = imgPath;\n    };\n    img.onerror = function() {\n        document.getElementById('repo_screenshot').src = '/repo_screenshots/screenshot.png';\n    };\n    img.src = imgPath;\n    $.each(themeInfo.info, function(key, value) {\n        var val = '<span class=\"preserve-line-breaks\">' + value + '</span>';\n        var listItem = $('<li>').html(key + ': ' + val);\n        $themeDescriptionContent.append(listItem);\n    });\n}\n\nfunction theme_download_select() {\n    var theme = document.getElementById(\"theme-downloader-selector\").value;\n    \n    $('#theme_downloader').find('select, button').prop('disabled', true);\n\n    var themes = window.themes; \n    var version = themes[theme]?.info?.version || 'Unknown'; \n    var json = {\n        \"theme\": theme,\n        \"version\": version\n    };\n    sendJSON(\"Fancygotchi/version_compare\", json, function(response) {\n        data = JSON.parse(response.responseText)\n        var localVersion = data.local_version || 'Unknown';\n        var isNewer = data.is_newer;\n        if (isNewer) {\n            var message = `A newer ${theme} version (${version}) is available. Your current version is ${localVersion}. Would you like to update?`;\n        } \n        if (!isNewer) {\n            var message = `You have the ${theme} version ${localVersion} installed. The available version is ${version}. Do you want to overwrite your current version?`;\n        }\n        if (isNewer == null) {\n            var message = `You will download ${theme} version ${version}. Do you want to peoceed?`;\n        }\n        var confirmOverwrite = confirm(message);\n        if (confirmOverwrite) {\n            var json = {\n                \"theme\": theme,\n            };\n            $('#loading-spinner').show();\n            $('#loading-spinner p').text(\"Downloading...\");\n            sendJSON(\"Fancygotchi/theme_download_select\", json, function(response) {\n                if (response.status == 200) {\n                    $('#loading-spinner').hide();\n                    alert(\"Theme updated successfully!\");\n                    theme_list();\n                } else {\n                    $('#loading-spinner').hide();\n                    alert(\"There was an error updating the theme.\");\n                }\n                $('#theme_downloader').find('select, button').prop('disabled', false);\n            });\n        }\n    });\n}\n\nfunction sendJSON(url, data, callback) {\n    var xobj = new XMLHttpRequest();\n    var csrf = \"{{ csrf_token() }}\";\n    xobj.open('POST', url);\n    xobj.setRequestHeader(\"Content-Type\", \"application/json\");\n    xobj.setRequestHeader('x-csrf-token', csrf);\n    xobj.onreadystatechange = function () {\n        if (xobj.readyState == 4) {\n            callback(xobj);\n        }\n    };\n    xobj.send(JSON.stringify(data));\n}\n\nfunction sendFormData(url, formData, callback) {\n    var xhr = new XMLHttpRequest();\n    var csrf = \"{{ csrf_token() }}\";\n    xhr.open('POST', url);\n    xhr.setRequestHeader('x-csrf-token', csrf);\n    xhr.onreadystatechange = function() {\n        if (xhr.readyState === XMLHttpRequest.DONE) {\n            console.log(\"Response received:\", xhr.responseText); \n            if (xhr.status === 200) {\n                document.getElementById('zipFile').value = '';\n                document.getElementById('message').innerHTML = 'Upload successful!';\n                callback(null, xhr.responseText);\n            } else {\n                console.error('Request failed with status:', xhr.status);\n                document.getElementById('message').innerHTML = 'Upload error: ' + xhr.responseText;\n                callback(new Error('Request failed with status: ' + xhr.status), null);\n            }\n        }\n    };\n\n    xhr.send(formData);\n}\n\nfunction loadJSON(url, callback) {\n    var xobj = new XMLHttpRequest();\n    xobj.overrideMimeType(\"application/json\");\n    xobj.open('GET', url, true);\n    xobj.onreadystatechange = function () {\n        if (xobj.readyState == 4 && xobj.status == \"200\") {\n            callback(JSON.parse(xobj.responseText));\n        }\n        if (xobj.readyState == 4 && xobj.status == \"500\") {\n            callback(JSON.parse(xobj.responseText));\n        }\n        if (xobj.readyState == 4 && xobj.status == \"404\") {\n            callback(JSON.parse(xobj.responseText));\n        }\n    };\n    xobj.send(null);\n}\n\nfunction flattenJson(data) {\n    var result = {};\n\n    function recurse(cur, prop) {\n        if (Array.isArray(cur)) {\n            result[prop] = cur.map(function(item) {\n                return typeof item === 'string' ? `\"${item}\"` : item;\n            });\n        } else if (Object(cur) !== cur) {\n            result[prop] = cur;\n        } else {\n            for (var p in cur) {\n                recurse(cur[p], prop ? prop + \".\" + p : p);\n            }\n        }\n    }\n\n    recurse(data, \"\");\n    return result;\n}\n\nfunction jsonToArray(json) {\n    var theme_array = [];\n    var x = 0;\n    Object.keys(json).forEach(function(key) {\n        theme_array[x] = [key, json[key]];\n        x+=1;\n    });\n    return theme_array;\n}\nfunction openDeleteDialog() {\n    $('#delete-dialog').popup('open');\n}\nfunction theme_delete() {\n    var theme = document.getElementById(\"theme-selector\").value;\n    active_theme(function(activeTheme) {\n        if (theme !== \"Default\" && theme !== activeTheme) {\n            openDeleteDialog();\n        } else {\n            alert('Cannot delete default theme or the active theme.');\n        }\n    });\n}\n\nfunction display_hijack() {\n    loadJSON(\"Fancygotchi/display_hijack\",function(response) {\n        if (response.status == \"200\") {\n            alert(\"Screen Hijacked\");\n        } else {\n            alert(\"Error while hijacking the display (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction display_pwny() {\n    loadJSON(\"Fancygotchi/display_pwny\", function(response) {\n        if (response.status == \"200\") {\n            alert(\"Screen Pwny\");\n        } else {\n            alert(\"Error while diplaying pwagotchi (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction display_next() {\n    loadJSON(\"Fancygotchi/display_next\",function(response) {\n        if (response.status == \"200\") {\n            alert(\"Next second screen mode\");\n        } else {\n            alert(\"Error while diplaying next second screen mode (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction display_previous() {\n    loadJSON(\"Fancygotchi/display_previous\", function(response) {\n        if (response.status == \"200\") {\n            alert(\"Next second screen mode\");\n        } else {\n            alert(\"Error while diplaying previous second screen mode (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction screen_saver_next() {\n    loadJSON(\"Fancygotchi/screen_saver_next\", function(response) {\n        if (response.status == \"200\") {\n            alert(\"Next screen saver\");\n        } else {\n            alert(\"Error while diplaying next screen saver (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction screen_saver_previous() {\n    loadJSON(\"Fancygotchi/screen_saver_previous\", function(response) {\n        if (response.status == \"200\") {\n            alert(\"Previous screen saver\");\n        } else {\n            alert(\"Error while diplaying previous screen saver (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction stealth() {\n    loadJSON(\"Fancygotchi/stealth\", function(response) {\n        if (response.status == \"200\") {\n            alert(\"Stealth mode\");\n        } else {\n            alert(\"Error while enabling stealth mode (err-code: \" + response.status + \")\");\n        }\n    });\n}\nfunction navigate(btn) {\n    var action = btn\n    var which_screen = document.getElementById(\"screen\").checked;\n    var screen = 1\n    if (which_screen == true) {\n        screen = 2\n    }\n    console.log(\"screen: \"+screen)\n    loadJSON(\"Fancygotchi/btn_cmd?action=\"+action+\"&hardware=False&screen=\"+screen,  function(response) {\n        if (response.status == \"200\") {\n            console.log(\"Navigation: \" + \"Fancygotchi/btn_cmd?action=\"+action+\"&hardware=False&screen=\"+screen);\n        } else {\n            console.log(\"Navigation error: \" + btn + \" (err-code: \" + response.status + \")\");\n        }\n    });\n}\n\nfunction glitchEffect(amplify = false) {\n    const lines = document.querySelectorAll('#fancygotchi span');\n    const numLines = lines.length;\n    \n    const numGlitches = amplify ? Math.floor(Math.random() * numLines) + 1 : Math.min(5, Math.floor(Math.random() * 5));\n    const indices = new Set();\n    \n    while (indices.size < numGlitches) {\n        indices.add(Math.floor(Math.random() * numLines));\n    }\n    \n    indices.forEach(index => {\n        const line = lines[index];\n        line.classList.add('glitch-line');\n        \n        const randomMove = Math.floor(Math.random() * 200) - 50; \n        line.style.transform = `translateX(${randomMove}px)`;  \n        \n        setTimeout(() => {\n            line.classList.remove('glitch-line');\n            line.style.transform = ''; \n        }, amplify ? 1000 : 300);\n    });\n}\ndocument.addEventListener('click', () => {\n    glitchEffect(true);\n});\n\nsetInterval(glitchEffect, 150);\n{% endblock %}\n\"\"\"\n\nCSS = \"\"\"\n.ui-image {\n    width: 100%;\n}\n\n.pixelated {\n    image-rendering: optimizeSpeed; /* Legal fallback */\n    image-rendering: -moz-crisp-edges; /* Firefox        */\n    image-rendering: -o-crisp-edges; /* Opera          */\n    image-rendering: -webkit-optimize-contrast; /* Safari         */\n    image-rendering: optimize-contrast; /* CSS3 Proposed  */\n    image-rendering: crisp-edges; /* CSS4 Proposed  */\n    image-rendering: pixelated; /* CSS4 Proposed  */\n    -ms-interpolation-mode: nearest-neighbor; /* IE8+           */\n}\n\n.image-wrapper {\n    flex: 1;\n    position: relative;\n}\n\ndiv.status {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n}\n\na.read {\n    color: #777 !important;\n}\n\np.messagebody {\n    padding: 1em;\n}\n\nli.navitem {\n    width: 16.66% !important;\n    clear: none !important;\n}\n\n/* Custom indentations are needed because the length of custom labels differs from\n   the length of the standard labels */\n.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {\n    text-indent: -5.9em;\n}\n\n.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {\n    text-indent: 0.5em;\n}\n\n/* Custom widths are needed because the length of custom labels differs from\n   the length of the standard labels */\n.custom-size-flipswitch.ui-flipswitch {\n    width: 8.875em;\n}\n\n.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {\n    padding-left: 7em;\n    width: 1.875em;\n}\n\n@media (min-width: 28em) {\n    /*Repeated from rule .ui-flipswitch above*/\n    .ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {\n        width: 1.875em;\n    }\n}\n\n#container {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-around;\n}\n\n.plugins-box {\n    margin: 0.5rem;\n    padding: 0.2rem;\n    border-style: groove;\n    border-radius: 0.5rem;\n    background-color: lightgrey;\n    text-align: center;\n}             \n\"\"\"\n\nBOOT_ANIM = \"\"\"import time\nfrom PIL import Image, ImageSequence\nimport os\nimport logging\nfrom pwnagotchi import utils\nimport pwnagotchi.ui.hw as hw\n\nfrom pwnagotchi.ui.hw import display_for\nimport argparse\n#import traceback\n\ndef setup_logging(log_file='/var/log/bootanim.log'):\n    # Ensure the directory exists\n    log_dir = os.path.dirname(log_file)\n    if not os.path.exists(log_dir):\n        os.makedirs(log_dir)\n\n    # Configure logging\n    logging.basicConfig(\n        filename=log_file,\n        level=logging.INFO,  # or DEBUG, WARNING, ERROR, CRITICAL\n        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n    )\n\ndef show_boot_animation(display_driver, config):\n    try:\n        frames_path = '{img_path}'\n        width = {width}\n        height = {height}\n        rotation = {rotation}\n\n        # Check if folder exists\n        if not os.path.exists(frames_path):\n            return\n\n        # Accept common image formats\n        valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif')\n        frames = sorted([f for f in os.listdir(frames_path) if f.lower().endswith(valid_extensions)])\n        logging.debug(\"Found %s frames\" % len(frames))\n        # Check if there are any images to process\n        if not frames:\n            return\n\n        frames_count = len(frames)\n\n        if len(frames) == 1:\n            if frames[0].lower().endswith('.gif'):\n                source_path = os.path.join(frames_path, frames[0])\n                with Image.open(source_path) as img:\n                    frames_count = sum(1 for _ in ImageSequence.Iterator(img))\n\n        max_loops = {max_loops}\n        total_duration = {total_duration}\n        start_time = time.time()\n        loop_count = 0\n\n        delay =  total_duration / (frames_count * max_loops)\n\n        while (time.time() - start_time < total_duration) or (loop_count < max_loops):\n            for frame in frames:\n                if (time.time() - start_time >= total_duration) and (loop_count >= max_loops):\n                    break\n                \n                frame_path = os.path.join(frames_path, frame)\n\n                if frame.lower().endswith('.gif'):\n                    logging.debug('Processing GIF: %s' % frame_path)\n                    with Image.open(frame_path) as img:\n                        for gif_frame in ImageSequence.Iterator(img):\n                            if rotation == 90:\n                                gif_frame = gif_frame.rotate(90, expand=True)\n                            elif rotation == 180:\n                                gif_frame = gif_frame.rotate(180, expand=True)\n                            elif rotation == 270:\n                                gif_frame = gif_frame.rotate(270, expand=True)\n                            gif_frame = gif_frame.resize((width, height)).convert('{color_mode}')\n                            logging.debug('Rendering frame: %s' % gif_frame)\n                            display_driver.render(gif_frame)\n                else:\n                    # Handle any other image formats (jpg, jpeg, bmp, png, etc.)\n                    with Image.open(frame_path) as img:\n                        if rotation == 90:\n                            img = img.rotate(90, expand=True)\n                        elif rotation == 180:\n                            img = img.rotate(180, expand=True)\n                        elif rotation == 270:\n                            img = img.rotate(270, expand=True)\n                        img = img.resize((width, height)).convert('{color_mode}')\n                        logging.debug('Rendering frame: %s' % img)\n                        display_driver.render(img)\n                        \n                time.sleep(delay)  # Adjust this value to control animation speed\n            logging.debug('Finished loop %d' % loop_count)\n            loop_count += 1\n            \n    except Exception as ex:\n        logging.error(ex)\n        #logging.error(traceback.format_exc())\n        display_driver.clear()\n\nif __name__ == \"__main__\":\n    setup_logging()\n    logging.debug('Starting boot animation...')\n    args = argparse.Namespace(\n        config='/etc/pwnagotchi/default.toml', \n        user_config='/etc/pwnagotchi/config.toml', \n        do_manual=False, \n        skip_session=False, \n        do_clear=False, \n        debug=False, \n        version=False, \n        print_config=False, \n        wizard=False, \n        check_update=False, \n        donate=False\n    )\n    logging.debug(args)\n    try:\n        config = utils.load_config(args)\n        logging.debug('Display config: %s' % config['ui']['display'])\n        logging.debug('Display type: %s' % config['ui'])\n        display_type = config['ui']['display']['type']\n        logging.debug('Display type: %s' % display_type)\n        display_driver = display_for(config)\n        logging.debug(vars(display_driver))\n        if display_driver is not None:\n            \n            display_driver.config['rotation'] = {rotation}\n            \n            if hasattr(display_driver, 'initialize'):\n                try:\n                    display_driver.initialize()\n                    show_boot_animation(display_driver, config)\n                    display_driver.config['enabled'] = True\n                    display_driver.is_initialized = True\n                except Exception as e:\n                    logging.error(e)\n            display_driver.config['enabled'] = False\n\n            if hasattr(display_driver, 'displayImpl') and display_driver.config.get('enabled', False):\n                display_driver.config['enabled'] = False\n                logging.debug('[Fancygotchi] Display has been disabled')\n                \n                if hasattr(display_driver, 'clear'):\n                    logging.debug('[Fancygotchi] Clearing the display')\n                    display_driver.clear()\n                \n                display_driver.is_initialized = False\n\n                if hasattr(display_driver, '_display'):\n                    logging.debug('[Fancygotchi] Resetting internal display reference')\n                    display_driver._display = None\n        else:\n            logging.error(\"Failed to initialize the display driver.\")\n    except KeyError as e:\n        logging.error('KeyError: %s' % e)\n        #logging.error(traceback.format_exc())\n        display_type = 'Unknown'\n\"\"\"\n\nFANCYTOOLS = \"\"\"#!{pyenv}\nimport time\nimport argparse\nimport os\nimport json\nimport subprocess\nimport requests\nimport toml\n\ndef create_log_directory():\n    log_dir = '/var/log/fancytools/'\n    if not os.path.exists(log_dir):\n        result = subprocess.run(['sudo', 'mkdir', '-p', log_dir], check=True, capture_output=True, text=True)\n        print(\"Directory %s created.\" % log_dir)\n    return log_dir\n\ndef get_credentials():\n    try:\n        with open('/etc/pwnagotchi/config.toml', 'r') as f:\n            config = toml.load(f)\n            return (config['ui']['web']['username'], config['ui']['web']['password'])\n    except:\n        return ('changeme', 'changeme')\n\ndef send_command(command_data):\n    username, password = get_credentials()\n    base_url = 'http://%s:%s@localhost:8080/plugins/Fancygotchi' % (username, password)\n\n    endpoint_map = {\n        'stealth_mode': '/stealth',\n        'second_screen': '/second_screen',\n        'switch_screen_mode': '/display_next',\n        'switch_screen_mode_reverse': '/display_previous',\n        'next_screen_saver': '/screen_saver_next',\n        'previous_screen_saver': '/screen_saver_previous',\n        'up': '/btn_cmd',\n        'down': '/btn_cmd',\n        'left': '/btn_cmd',\n        'right': '/btn_cmd',\n        'select': '/btn_cmd',\n        'start': '/btn_cmd',\n        'a': '/btn_cmd',\n        'b': '/btn_cmd',\n        'x': '/btn_cmd',\n        'y': '/btn_cmd',\n        'l1': '/btn_cmd',\n        'l2': '/btn_cmd',\n        'r1': '/btn_cmd',\n        'r2': '/btn_cmd',\n        'theme_select': '/theme_select',\n        'theme_refresh': '/theme_refresh',\n        'plugin': '/plugin',\n        'restart-auto': '/restart',\n        'restart-manu': '/restart',\n        'reboot-auto': '/reboot',\n        'reboot-manu': '/reboot',\n        'shutdown': '/shutdown'\n    }\n\n    action = command_data['action']\n    endpoint = endpoint_map.get(action)\n\n    if endpoint:\n        try:\n            query_params = ''\n            for key, value in command_data.items():\n                query_params += '%s=%s&' % (key, value)\n            query_params = query_params[:-1]  # remove trailing &\n            url = \"%s%s?%s\" % (base_url, endpoint, query_params)\n            print(url)\n            response = requests.get(url)\n\n            if response.status_code == 200:\n                print(\"Success: %s\" % action)\n            else:\n                print(\"Error: %s - %s\" % (response.status_code, response.text))\n\n        except Exception as e:\n            print(\"Error sending command: %s\" % e)\n            time.sleep(5)\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Fancytools\")\n    parser.add_argument('-d', '--diagnostic', nargs='*', dest='diagnostic_args',\n                        help='A full anonymized system report will be prompted. Additional arguments are accepted.')\n    parser.add_argument('-p', '--plugin', dest='plugin', help='Name of the plugin to toggle')\n    parser.add_argument('-e', '--enable', action='store_true', dest='enable',\n                        help='Enable the specified plugin (default is to disable)')\n    parser.add_argument('-r', '--restart', nargs='?', const='normal', dest='restart_mode',\n                        help='Restart the system (auto or manu)')\n    parser.add_argument('-b', '--reboot', nargs='?', const='normal', dest='reboot_mode',\n                        help='Reboot the system (auto or manu)')\n    parser.add_argument('-s', '--shutdown', action='store_true', dest='shutdown',\n                        help='Shutdown the system')\n    parser.add_argument('-B', '--button', choices=['start', 'up', 'down', 'left', 'right', 'select'], help='Control the menu')\n    parser.add_argument('-pr', '--refresh-plugins', action='store_true', help='Refresh installed plugins list')\n    parser.add_argument('-ts', '--theme-select', nargs=2, metavar=('NAME', 'ROTATION'), help='Select theme')\n    parser.add_argument('-tr', '--theme-refresh', action='store_true', help='Refresh theme')\n    parser.add_argument('-S', '--stealth-mode', action='store_true', help='Toggle stealth mode')\n    parser.add_argument('-sw', '--switch-screen-mode', choices=['next', 'previous'], help='Switch screen mode')\n    parser.add_argument('-s2', '--second-screen', action='store_true', help='Switch to second screen')\n    parser.add_argument('-sc', '--screen-saver', choices=['next', 'previous'], help='Switch screen saver')\n    parser.add_argument('-rb', '--run-bash', metavar='SCRIPT', help='Run a bash script')\n    parser.add_argument('-rp', '--run-python', metavar='FILE', help='Run a Python script')\n    \n    args = parser.parse_args()\n\n    log_dir = create_log_directory()\n\n    if args.diagnostic_args is not None:\n        script_path = os.path.abspath(__file__)\n        print(\"The path of the running script is: %s\" % script_path)\n        path = \"/usr/local/bin/diagnostic.sh\"\n        os.system(path)\n\n    if args.plugin:\n        enable_state = 'True' if args.enable else 'False'\n        command_data = {\n            'action': 'plugin',\n            'name': args.plugin,\n            'enable': enable_state\n        }\n        print(command_data)\n        send_command(command_data)\n\n    if args.restart_mode:\n        send_command({'action': f'restart-{args.restart_mode}'})\n\n    if args.reboot_mode:\n        send_command({'action': f'reboot-{args.reboot_mode}'})\n\n    if args.shutdown:\n        send_command({'action': 'shutdown'})\n\n    if args.button:\n        send_command({'action': args.button, 'hardware': True})\n\n    if args.refresh_plugins:\n        send_command({'action': 'refresh_plugins'})\n\n    if args.theme_select:\n        send_command({'action': 'theme_select', 'name': args.theme_select[0], 'rotation': args.theme_select[1]})\n\n    if args.theme_refresh:\n        send_command({'action': 'theme_refresh'})\n\n    if args.stealth_mode:\n        send_command({'action': 'stealth_mode'})\n\n    if args.switch_screen_mode:\n        action = 'switch_screen_mode' if args.switch_screen_mode == 'next' else 'switch_screen_mode_reverse'\n        send_command({'action': action})\n\n    if args.second_screen:\n        send_command({'action': 'second_screen'})\n\n    if args.screen_saver:\n        action = 'next_screen_saver' if args.screen_saver == 'next' else 'previous_screen_saver'\n        send_command({'action': action})\n\n    if args.run_bash:\n        send_command({'action': 'run_bash', 'file': args.run_bash})\n\n    if args.run_python:\n        send_command({'action': 'run_python', 'file': args.run_python})\n\nif __name__ == \"__main__\":\n    main()\n\"\"\"\n\nDIAGNOSTIC= \"\"\"#!/bin/bash\n\nget_log_file_path() {\n  local config_file=\"$1\"\n  if [ -f \"$config_file\" ]; then\n    log_path=$(grep '^main\\.log\\.path ' \"$config_file\" | cut -d'=' -f2 | tr -d ' \"')\n    if [ -n \"$log_path\" ]; then\n      echo \"$log_path\"\n      return\n    fi\n  fi\n  echo \"\"\n}\n\n# Get the script's directory\nscript_dir=$(dirname \"$(readlink -f \"$0\")\")\n\n# Output file in the script's directory\noutput_file=\"/var/log/fancytools/system_info.txt\"\n\n# Pwnagotchi version\necho \"Pwnagotchi version:\" > \"$output_file\"\npip list | grep pwnagotchi >> \"$output_file\"\necho >> \"$output_file\"\n\n# Kernel info\necho \"Kernel info:\" >> \"$output_file\"\nuname -a >> \"$output_file\"\necho >> \"$output_file\"\n\n# Boot config\necho \"Boot config:\" >> \"$output_file\"\n\n# Check for the presence of cmdline.txt and config.txt in /boot/firmware\ncmdline_file=\"/boot/firmware/cmdline.txt\"\nconfig_file=\"/boot/firmware/config.txt\"\n\nif [ -f \"$cmdline_file\" ]; then\n  cat \"$cmdline_file\" >> \"$output_file\"\nelse\n  # Fallback to /boot if not found in /boot/firmware\n  if [ -f \"/boot/cmdline.txt\" ]; then\n    cat \"/boot/cmdline.txt\" >> \"$output_file\"\n  else\n    echo \"cmdline.txt not found.\" >> \"$output_file\"\n  fi\nfi\necho >> \"$output_file\"\nif [ -f \"$config_file\" ]; then\n  cat \"$config_file\" >> \"$output_file\"\nelse\n  # Fallback to /boot if not found in /boot/firmware\n  if [ -f \"/boot/config.txt\" ]; then\n    cat \"/boot/config.txt\" >> \"$output_file\"\n  else\n    echo \"config.txt not found.\" >> \"$output_file\"\n  fi\nfi\necho >> \"$output_file\"\n\n# Service status\necho \"Service status:\" >> \"$output_file\"\nservice pwnagotchi status >> \"$output_file\"\necho >> \"$output_file\"\n\n# Network driver interface load\necho \"Network driver interface load:\" >> \"$output_file\"\nsudo dmesg | grep brcm >> \"$output_file\"\necho >> \"$output_file\"\n\n# List all IP active host names\necho \"List all IP active host names:\" >> \"$output_file\"\nhostname -I >> \"$output_file\"\necho >> \"$output_file\"\n\necho \"List all active ports:\" >> \"$output_file\"\nlsof -nP -iTCP -sTCP:LISTEN >> \"$output_file\"\necho >> \"$output_file\"\n\n# List available plugins\necho \"List available plugins:\" >> \"$output_file\"\ncat /etc/pwnagotchi/config.toml | grep plugin | grep enabled >> \"$output_file\"\necho >> \"$output_file\"\n\n# List enabled plugins\necho \"List enabled plugins:\" >> \"$output_file\"\ncat /etc/pwnagotchi/config.toml | grep plugin | grep enabled | grep true >> \"$output_file\"\necho >> \"$output_file\"\n\n# Attempt to find the log file path in the preferred config file\nlog_file=$(get_log_file_path \"/etc/pwnagotchi/config.toml\")\n\n# If not found, check the default config file\nif [ -z \"$log_file\" ]; then\n  log_file=$(get_log_file_path \"/etc/pwnagotchi/default.toml\")\nfi\n\n# Check if log file path was found\nif [ -z \"$log_file\" ]; then\n  log_file=\"/var/log/pwnagotchi.log\"\nfi\n\n# Config file\nconfig_file=\"/etc/pwnagotchi/config.toml\"\n\n# Output files in the /var/log directory\nlog_dir=\"/var/log/fancytools/\"\nif [ ! -d \"$log_dir\" ]; then\n  echo \"Creating log directory: $log_dir\"\n  sudo mkdir -p \"$log_dir\" || { echo \"Failed to create $log_dir\"; exit 1; }\n  echo \"Directory $log_dir created.\"\nfi\n\nlog_output_file=\"$log_dir/anonymized_log.txt\"\nconfig_output_file=\"$log_dir/anonymized_config.toml\"\n\n# Ensure we have write access to log files\nif [ ! -w \"$log_dir\" ]; then\n  echo \"Cannot write to $log_dir. Please check permissions.\"\n  exit 1\nfi\n\n# Anonymize and export the last 100 lines of the log file to a file\nif [ -f \"$log_file\" ]; then\n  echo \"Anonymized log (last 100 lines):\"\n  tail -n 100 \"$log_file\" | sed -E -e 's/([0-9]{1,3}\\.){3}[0-9]{1,3}/XX.XX.XX.XX/g' -e 's/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/XX:XX:XX:XX:XX:XX/g' -e '/api_key/ s/=.*$/= \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"/' -e '/whitelist/ {s/=.*/= \\[\\]/; :loop n; /\\]/! {s/^[[:space:]]*[\"'\"'\"'].*[\"'\"'\"'],?//; s/^[[:space:]]*\\][[:space:]]*$//; b loop}}' -e '/password/ s/=.*$/= \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"/' -e 's/@[^()]*()/@XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/' > \"$log_output_file\"\nelse\n  echo \"Log file $log_file not found.\"\n  exit 1\nfi\n\n# Anonymize and export the config file to a file\nif [ -f \"$config_file\" ]; then\n  echo -e \"\\nAnonymized config file:\"\n  sed -E -e 's/([0-9]{1,3}\\.){3}[0-9]{1,3}/XX.XX.XX.XX/g' -e 's/([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}/XX:XX:XX:XX:XX:XX/g' -e '/api_key/ s/=.*$/= \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"/' -e '/whitelist/ {s/=.*/= \\[\\]/; :loop n; /\\]/! {s/^[[:space:]]*[\"'\"'\"'].*[\"'\"'\"'],?//; s/^[[:space:]]*\\][[:space:]]*$//; b loop}}' -e '/password/ s/=.*$/= \"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\"/' \"$config_file\" > \"$config_output_file\"\nelse\n  echo \"Config file $config_file not found.\"\n  exit 1\nfi\n\ncat $output_file\ncat $log_output_file\ncat $config_output_file\n\necho \"Basic system info saved to $output_file\"\necho \"Anonymized log saved to $log_output_file\"\necho \"Anonymized config saved to $config_output_file\"\n\"\"\"\n\nFANCYDISPLAY = '/var/tmp/pwnagotchi/FancyDisplay.png'\n\nclass FancyDisplay:\n    _instance = None\n\n    def __new__(cls, *args, **kwargs):\n        if not cls._instance:\n            cls._instance = super(FancyDisplay, cls).__new__(cls)\n        return cls._instance\n\n    def __init__(self, enabled=False, fps=1, th_path='', mode='screen_saver', sub_mode='show_logo', config={}):\n        self.enabled = enabled\n        self.image_lock = threading.Lock()\n        self.is_image_locked = False\n        self.th_path = th_path\n        self.displayImpl = None\n        self.hijack_frame = None\n        self.task = None\n        self.loop = None\n        self.thread = None\n        self.is_running_event = asyncio.Event()\n        self.stop_event = threading.Event()\n        self.running = False\n        self.fps = fps\n        self.fb = self.find_fb_device()\n        self.current_mode = mode\n        self.current_screen_saver = sub_mode\n        self.modes = ['screen_saver', 'auxiliary', 'terminal']\n        self.screen_saver_modes = ['show_logo', 'moving_shapes', 'random_colors', 'hyper_drive', 'show_animation']\n        if config: self.screen_data = config\n        else: self.screen_data = {}\n        self.set_mode(mode, sub_mode)\n        logging.info(\"[FancyDisplay] FancyDisplay initialized.\")\n\n    def _start_loop(self):\n        logging.info(\"[FancyDisplay] Starting the asyncio event loop in a new thread.\")\n        asyncio.set_event_loop(self.loop)\n        self.is_running_event.set()\n        try:\n            self.loop.run_until_complete(self.screen_controller())\n        except asyncio.CancelledError:\n            pass\n        finally:\n            self.loop.close()\n            self.is_running_event.clear()\n\n    def start(self, res, rot, col):\n        logging.debug(\"[FancyDisplay] Starting display controller.\")\n        self._res = res\n        self._rot = rot\n        self._col = col\n        self.displayImpl = self.display_hijack()\n\n        if self.loop is None or self.loop.is_closed():\n            self.loop = asyncio.new_event_loop()\n            self.thread = threading.Thread(target=self._start_loop, daemon=True)\n            self.thread.start()\n\n        while not self.is_running_event.is_set():\n            time.sleep(0.1)\n\n    def stop(self):\n        self.running = False\n        if self.loop and not self.loop.is_closed():\n            self.loop.call_soon_threadsafe(self.loop.stop)\n        if self.thread:\n            self.thread.join()\n        self.loop = None\n        self.thread = None\n        logging.debug(\"[FancyDisplay] Display controller stopped.\")\n\n    async def screen_controller(self):\n        self.running = True\n        while self.running:\n            await self.refacer()\n            await asyncio.sleep(0.1)\n\n    def is_running(self):\n        if self.is_running_event is not None:\n            return self.is_running_event.is_set()\n        logging.error(\"[FancyDisplay] is_running_event is not initialized.\")\n        return False\n\n    def cleanup(self):\n        logging.debug(\"[FancyDisplay] Cleaning up the FancyDisplay resources.\")\n        self.task = None\n        if self.loop is not None:\n            if not self.loop.is_closed():\n                logging.debug(\"[FancyDisplay] Closing event loop.\")\n                self.loop.close()\n        self.loop = None\n        self.thread = None\n        self.displayImpl = None\n        self.hijack_frame = None\n        self.screen_data = {}\n      \n    def _calculate_aspect_ratio(self, width, height, aspect_ratio):\n        if width < height:\n            new_width = width\n            new_height = int(new_width / aspect_ratio)\n        else:\n            new_height = height\n            new_width = int(new_height * aspect_ratio)\n        return new_width, new_height\n\n    def screen(self):\n        return  self.hijack_frame\n\n    async def refacer(self):\n        try: \n            fps = 1 / self.fps \n            refresh_interval = 1\n            iteration = 0\n            while self.running:\n                if iteration % refresh_interval == 0:\n                    self.hijack_frame = self.get_mode_image()\n\n                if self.hijack_frame is not None:\n                    canvas = self.hijack_frame\n                    if self._rot == 90:\n                        canvas = canvas.rotate(90, expand=True)\n                    elif self._rot == 180:\n                        canvas = canvas.rotate(180, expand=True)\n                    elif self._rot == 270:\n                        canvas = canvas.rotate(270, expand=True)\n\n                    if self.enabled:\n                        canvas = canvas.resize((self._res[0], self._res[1])).convert(self._col)\n                        self.displayImpl.render(canvas)\n                else:\n                    logging.warning(\"[FancyDisplay] No image to display.\")\n                \n                await asyncio.sleep(fps)\n                iteration += 1\n\n        except asyncio.CancelledError:\n            logging.warning(\"[FancyDisplay] refacer cancelled.\")\n    def display_hijack(self):\n        try:\n            args = argparse.Namespace(\n                config='/etc/pwnagotchi/default.toml', \n                user_config='/etc/pwnagotchi/config.toml', \n                do_manual=False, \n                skip_session=False, \n                do_clear=False, \n                debug=False, \n                version=False, \n                print_config=False, \n                wizard=False, \n                check_update=False, \n                donate=False\n            )\n            config = utils.load_config(args)\n            display_type = config['ui']['display']['type']\n            display = config['ui']['display']['enabled']\n            self.displayImpl = None\n\n            displayImpl = getattr(self, 'displayImpl', None)\n            if not displayImpl or not displayImpl.config.get('enabled', False):\n                self.displayImpl = display_for(config)\n                self.displayImpl.config['rotation'] = 0\n                logging.debug(self.displayImpl.config)\n\n                if hasattr(self.displayImpl, 'initialize') or not self.enabled:\n                    logging.debug('[Fancygotchi] Initializing display')\n                    if self.enabled:\n                        self.displayImpl.initialize()\n                    self.displayImpl.config['enabled'] = True\n                    return self.displayImpl\n                else:\n                    logging.debug('[Fancygotchi] Failed to initialize display: No initialization method found.')\n            else:\n                logging.debug('[Fancygotchi] Display is already initialized.')\n\n        except KeyError as e:\n            logging.error(f'[FancyDisplay] KeyError while display hijacking: {e}')\n            logging.error(traceback.format_exc())\n            \n    def glitch_text_effect(self, text, glitch_chance=0.2, max_spaces=3):\n        lines = text.split('\\n')\n        glitched_lines = []\n\n        for line in lines:\n            if random.random() < glitch_chance: \n                num_spaces = random.randint(1, max_spaces) \n                line = ' ' * num_spaces + line \n\n            glitched_lines.append(line)\n\n        return '\\n'.join(glitched_lines)\n\n    def set_mode(self, mode, sub_mode=None, config={}):\n        if mode in self.modes:\n            logging.debug(f\"[FancyDisplay] Switching to mode: {mode}\")\n            self.current_mode = mode\n            if mode == \"screen_saver\":\n                self.set_screen_saver_mode(sub_mode)\n                self.screen_cdata = config\n            elif mode == \"auxiliary\":\n                self.screen_data = config\n            elif mode == \"terminal\":\n                self.screen_data = config \n        else:\n            logging.warning(f\"[FancyDisplay] Invalid mode: {mode}. Available modes are: {self.modes}\")\n    \n    def switch_mode(self, direction='next'):\n        current_index = self.modes.index(self.current_mode)\n        sub_mode = None\n        if direction == 'next':\n            next_index = (current_index + 1) % len(self.modes)\n        elif direction == 'previous':\n            next_index = (current_index - 1) % len(self.modes)\n        else:\n            logging.warning(f\"[FancyDisplay] Invalid direction: {direction}. Using 'next' as default.\")\n            next_index = (current_index + 1) % len(self.modes)\n        \n        next_mode = self.modes[next_index]\n        \n        logging.debug(f\"[FancyDisplay] Switching to the {direction} mode: {next_mode}\")\n        if next_mode == \"screen_saver\": \n            sub_mode = self.current_screen_saver\n        self.set_mode(next_mode, sub_mode)\n        self.set_screen_saver_mode(sub_mode)\n        self.current_mode = next_mode\n        return next_mode\n\n    def find_fb_device(self):\n        for i in range(10): \n            fb_device = f\"/dev/fb{i}\"\n            if os.path.exists(fb_device):\n                return fb_device\n        return None\n\n    def get_fb_size(self):\n        import subprocess\n        output = subprocess.check_output(['fbset', '-s']).decode('utf-8')\n        for line in output.split('\\n'):\n            if 'geometry' in line:\n                parts = line.split()\n                return int(parts[1]), int(parts[2])\n        return self._res[0], self._res[1] \n\n    def read_fb(self, width, height):\n        with open(self.fb, \"rb\") as fb:\n                return memoryview(fb.read(width * height * 2))\n\n    def terminal_mode(self):\n        if self.fb is None:\n            return self.show_logo()\n\n        fb_width, fb_height = self.get_fb_size()\n        fb_data = self.read_fb(fb_width, fb_height)\n        \n        rgb_image = self.convert_to_rgb(fb_data, fb_width, fb_height)\n        image = Image.fromarray(rgb_image, mode='RGB')\n        \n        width, height = self._res\n        resized_image = image.resize((width, height), Image.BILINEAR)\n        \n        return resized_image\n\n    def convert_to_rgb(self, fb_data, width, height):\n        rgb_array = np.zeros((height, width, 3), dtype=np.uint8)\n        pixels = np.frombuffer(fb_data, dtype=np.uint16)\n        \n        r = ((pixels >> 11) & 0x1F) << 3\n        g = ((pixels >> 5) & 0x3F) << 2\n        b = (pixels & 0x1F) << 3\n        \n        rgb_array[..., 0] = r.reshape(height, width)\n        rgb_array[..., 1] = g.reshape(height, width)\n        rgb_array[..., 2] = b.reshape(height, width)\n        \n        return rgb_array\n\n    def set_screen_saver_mode(self, sub_mode):\n        if sub_mode is None:\n            sub_mode = self.current_screen_saver\n        if sub_mode in self.screen_saver_modes:\n            logging.debug(f\"[FancyDisplay] Switching screen_saver to: {sub_mode}\")\n            self.current_screen_saver = sub_mode\n            if sub_mode == 'show_logo':\n                options = {}\n            elif sub_mode == 'moving_shapes':\n                options = {\n                    \"shape_type\": \"text\", \n                    \"text\": \"Fancygotchi\", \n                    \"font_path\": \"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", \n                    \"color\": \"red\", \n                    \"speed\": 10, \n                    \"font_size\": 15,\n                }\n            elif sub_mode == 'random_colors':\n                options = {\n                    \"speed\": 1,\n                }\n            elif sub_mode == 'hyper_drive':\n                num_stars = 100 \n                options = {\n                    'stars': [\n                        {\n                            'position': [random.randint(-self._res[0]//2, self._res[0]//2), random.randint(-self._res[1]//2, self._res[1]//2)],\n                            'velocity': random.uniform(2, 5),  \n                            'size': random.uniform(1, 3),\n                            'streak_length': random.uniform(5, 20),\n                            'color': 'white'\n                        } for _ in range(num_stars)\n                    ],\n                    'speed': 1.0 \n                }\n            elif sub_mode == 'show_animation':\n                frames_path = os.path.join(self.th_path, 'img', 'boot') if self.th_path else ''\n                options = {\n                    'frames_path': frames_path,\n                    'max_loops': 1,\n                    'total_duration': 10,\n                }\n            self.screen_data.update(options)\n            logging.info(f\"[FancyDisplay] Screen saver options: {self.screen_data}\")\n        else:\n            logging.warning(f\"[FancyDisplay] Invalid screen_saver sub-mode: {sub_mode}. Available sub-modes are: {self.screen_saver_modes}\")\n\n    \n    def switch_screen_saver_submode(self, direction='next'):\n        if self.current_mode != 'screen_saver':\n            logging.warning(f\"[FancyDisplay] Not in screen_saver mode. Current mode is: {self.current_mode}\")\n            return self.current_mode\n        \n        current_index = self.screen_saver_modes.index(self.current_screen_saver)\n        \n        if direction == 'next':\n            next_index = (current_index + 1) % len(self.screen_saver_modes) \n        elif direction == 'previous':\n            next_index = (current_index - 1) % len(self.screen_saver_modes)  \n        else:\n            logging.error(f\"[FancyDisplay] Invalid direction: {direction}. Must be 'next' or 'previous'.\")\n            return self.current_mode\n        \n        next_submode = self.screen_saver_modes[next_index]\n        logging.warning(f\"[FancyDisplay] Switching to the {direction} screen_saver sub-mode: {next_submode}\")\n        self.set_screen_saver_mode(next_submode)\n        return next_submode\n\n    def get_mode_image(self):\n        logging.debug(f\"[FancyDisplay] Getting mode image: {self.current_mode}\")\n        if self.current_mode == 'screen_saver':\n            return self.get_screen_saver_image()\n        elif self.current_mode == 'auxiliary':\n            return self.auxiliary_image()\n        elif self.current_mode == 'terminal':\n            return self.terminal_mode()\n        else:\n            logging.warning(f\"[FancyDisplay] Unknown mode: {self.current_mode}. Falling back to default.\")\n            return self.show_logo()\n\n    def get_screen_saver_image(self):\n        if self.current_screen_saver == 'show_logo':\n            return self.show_logo() \n        elif self.current_screen_saver == 'moving_shapes':\n            return self.moving_shapes_screen_saver()\n        elif self.current_screen_saver == 'random_colors':\n            return self.random_colors_screen_saver()\n        elif self.current_screen_saver == 'hyper_drive':\n            return self.hyperdrive_screen_saver()\n        elif self.current_screen_saver == 'show_animation':\n            return self.show_animation_screen_saver()\n        else:\n            logging.warning(f\"[FancyDisplay] Unknown screen_saver sub-mode: {self.current_screen_saver}.\")\n            self.current_screen_saver = 'show_logo'\n            return self.show_logo() \n\n\n    def auxiliary_image(self):\n        image = self.show_logo()\n        draw = ImageDraw.Draw(image)\n        font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 12)\n        text = \"Auxiliary mode\"\n        text_color = (255, 0, 0) \n        image_width, image_height = image.size\n        try:\n            text_width, text_height = draw.textsize(text, font)\n        except:\n            _, _, text_width, text_height = draw.textbbox((0, 0),text, font)\n        position = ((image_width - text_width) // 2, 10)\n        draw.text(position, text, font=font, fill=text_color)\n        return image\n\n    def show_logo(self):\n        try:\n            width = self._res[0]\n            height = self._res[1]\n            canvas = Image.new('RGBA', (width, height), 'black')\n            draw = ImageDraw.Draw(canvas)\n            font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 3)\n            text = self.glitch_text_effect(LOGO, glitch_chance=0.25, max_spaces=5)\n            try:\n                text_width, text_height = draw.textsize(text, font=font)\n            except:\n                _, _, text_width, text_height = draw.textbbox((0, 0), text, font=font)\n            logo_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))\n            draw_logo = ImageDraw.Draw(logo_img)\n            draw_logo.text((0, 0), text, fill='lime', font=font)\n            aspect_ratio = text_width / text_height\n            new_width, new_height = self._calculate_aspect_ratio(width, height, aspect_ratio)\n            resized_logo = logo_img.resize((new_width, new_height))\n            x = (width - new_width) // 2\n            y = (height - new_height) // 2\n            canvas.paste(resized_logo, (x, y), resized_logo)\n            self.hijack_frame = canvas\n            return canvas\n        except KeyError as e:\n            logging.debug(f'[FancyDisplay] KeyError while showing logo: {e}')\n            logging.debug(traceback.format_exc())\n\n    def moving_shapes_screen_saver(self):\n        try:\n            font_path = self.screen_data.get('font_path')\n            font_size = self.screen_data.get('font_size')\n            shape_type = self.screen_data.get('shape_type')\n            text = self.screen_data.get('text')\n            color = self.screen_data.get('color')\n            speed = self.screen_data.get('speed')\n\n            width, height = self._res\n            font = ImageFont.truetype(font_path, font_size)\n\n            if shape_type == \"text\":\n                try:\n                    shape_width, shape_height = font.getsize(text)\n                except:\n                    _, _, shape_width, shape_height = font.getbbox(text)\n            else:\n                shape_width = shape_height = shape_size \n            if not hasattr(self, 'shape_position'):\n                self.shape_position = [random.randint(0, width - shape_width), random.randint(0, height - shape_height)]\n                self.shape_velocity = [random.choice([-1, 1]) * speed, random.choice([-1, 1]) * speed] \n            x, y = self.shape_position\n            vx, vy = self.shape_velocity\n            if x + shape_width >= width or x <= 0:\n                vx = -vx\n            if y + shape_height >= height or y <= 0:\n                vy = -vy\n            x += vx\n            y += vy\n            self.shape_position = [x, y]\n            self.shape_velocity = [vx, vy]\n\n            canvas = Image.new('RGBA', (width, height), 'black')\n            draw = ImageDraw.Draw(canvas)\n\n            if shape_type == \"text\":\n                draw.text((x, y), text, font=font, fill=color)\n            else:\n                draw.ellipse((x, y, x + shape_width, y + shape_height), fill=color)\n            return canvas\n        except KeyError as e:\n            logging.error(f'[FancyDisplay] KeyError while moving shapes: {e}')\n            logging.error(traceback.format_exc())\n\n    def random_colors_screen_saver(self):\n        speed = self.screen_data.get('speed')\n        width, height = self._res\n        canvas = Image.new('RGBA', (width, height), (\n            random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), 255))\n        time.sleep(speed)\n        return canvas\n\n    def hyperdrive_screen_saver(self):\n        width, height = self._res\n        canvas = Image.new('RGBA', (width, height), 'black')\n        draw = ImageDraw.Draw(canvas)\n        \n        center_x, center_y = width // 2, height // 2\n        speed = self.screen_data.get('speed', 1.0)\n        \n        stars = self.screen_data['stars']\n        \n        for star in stars:\n            pos_x, pos_y = star['position']\n            velocity = star['velocity'] * speed \n            size = star['size']\n            streak_length = star['streak_length']\n            \n            pos_x *= (1 + velocity / 100)\n            pos_y *= (1 + velocity / 100)\n            \n            streak_end_x = pos_x * (1 + streak_length / 100)\n            streak_end_y = pos_y * (1 + streak_length / 100)\n\n            size = min(size * (1 + velocity / 10), 10)\n            \n            draw.line([(center_x + streak_end_x, center_y + streak_end_y), \n                    (center_x + pos_x, center_y + pos_y)], fill=star['color'], width=int(size))\n            \n            if abs(pos_x) > width // 2 or abs(pos_y) > height // 2:\n                star['position'] = [random.randint(-50, 50), random.randint(-50, 50)]\n                star['velocity'] = random.uniform(2, 5)\n                star['size'] = random.uniform(1, 3)\n                star['streak_length'] = random.uniform(5, 20)\n                \n                pos_x, pos_y = star['position']\n                velocity = star['velocity'] * speed\n                pos_x *= (1 + velocity / 100)\n                pos_y *= (1 + velocity / 100)\n                streak_end_x = pos_x * (1 + star['streak_length'] / 100)\n                streak_end_y = pos_y * (1 + star['streak_length'] / 100)\n                \n                draw.line([(center_x + streak_end_x, center_y + streak_end_y), \n                        (center_x + pos_x, center_y + pos_y)], fill=star['color'], width=int(star['size']))\n\n            star['position'] = [pos_x, pos_y]\n        \n        return canvas\n\n    def show_animation_screen_saver(self):\n        try:\n            if self.screen_data is None:\n                logging.error(\"[FancyDisplay] screen_data is None. Unable to show animation screen saver.\")\n                return self.show_logo() \n                \n            frames_path = self.screen_data.get('frames_path', '')\n            max_loops = self.screen_data.get('max_loops', 1)\n            total_duration = self.screen_data.get('total_duration', 10)\n            target_fps = 24\n            frame_duration = 0.2\n\n            if not frames_path or not os.path.exists(frames_path):\n                image = self.show_logo()\n                return image\n\n            valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif')\n            frames = sorted([f for f in os.listdir(frames_path) if f.lower().endswith(valid_extensions)])\n            \n            if not frames:\n                logging.error(\"[FancyDisplay] No valid frames found in the specified directory\")\n                return None\n\n            if not hasattr(self, 'animation_state'):\n                self.animation_state = {\n                    'start_time': time.time(),\n                    'loop_count': 0,\n                    'extracted_frames': []\n                }\n\n            current_time = time.time()\n            elapsed_time = current_time - self.animation_state['start_time']\n\n            if (self.animation_state['loop_count'] >= max_loops):\n                self.animation_state['start_time'] = current_time\n                self.animation_state['loop_count'] = 0\n                self.animation_state['extracted_frames'] = []\n\n            if not self.animation_state['extracted_frames']:\n                for frame in frames:\n                    frame_path = os.path.join(frames_path, frame)\n                    if frame.lower().endswith('.gif'):\n                        with Image.open(frame_path) as img:\n                            for gif_frame in ImageSequence.Iterator(img):\n                                self.animation_state['extracted_frames'].append(copy.deepcopy(gif_frame))\n                    else:\n                        self.animation_state['extracted_frames'].append(Image.open(frame_path))\n                \n                logging.debug(f\"[FancyDisplay] Extracted {len(self.animation_state['extracted_frames'])} frames\")\n\n            total_frames = len(self.animation_state['extracted_frames'])\n            current_frame_index = int((elapsed_time / frame_duration) % total_frames)\n\n            current_frame = self.animation_state['extracted_frames'][current_frame_index]\n\n            image = current_frame.resize((self._res[0], self._res[1])).convert(self._col)\n\n            if current_frame_index == 0 and elapsed_time > 0: \n                self.animation_state['loop_count'] += 1\n\n            if image is None:\n                image = self.show_logo()\n            return image\n\n        except Exception as ex:\n            logging.error(f\"[FancyDisplay] Error in show_animation_screen_saver: {ex}\")\n            logging.error(traceback.format_exc())\n            return None\n\nclass FancyMenu:\n    def __init__(self, fancygotchi, menu_theme, custom_menus={}):\n        \n        self._fancygotchi = fancygotchi\n        self.menus = copy.deepcopy(MENUS)\n        self.scroll_state = {}\n        self.menu_theme = menu_theme\n        self.menu_stack = [self.menus['Main menu']]\n        self.active = False\n        self.timeout = menu_theme['timeout']\n\n        self.last_activity_time = time.time()\n        self.plugin_names = get_all_plugin_names(self._fancygotchi)\n        self.populate_plugins_menu(self.plugin_names)\n        self.populate_themes_menu()\n        if custom_menus != {}:\n            self.load_menu_config(custom_menus)\n\n        self.reset_menus(custom_menus)\n\n    def reset_menus(self, custom_menus={}):\n        self.menus = copy.deepcopy(MENUS) \n        self.menu_stack = [self.menus['Main menu']]\n        self.populate_plugins_menu(self.plugin_names)\n        self.populate_themes_menu()\n        if custom_menus:\n            self.load_menu_config(custom_menus)\n\n    def load_menu_config(self, config):\n        menus = {}\n        main = {}\n        issues = []\n        for menu_key, menu_data in config.items():\n            if not isinstance(menu_data, dict):\n                issues.append(f\"[FancyMenu] Menu data for '{menu_key}' is not a dictionary.\")\n                continue\n            menu_title = menu_data.get(\"options\", {}).get(\"title\", menu_key)\n            action = {\"action\": \"submenu\", \"name\": menu_title}\n            if not menu_contains_button(self.menu_stack[0], menu_title):\n                self.menu_stack[0].add_button(menu_title, action)\n            menu_title = menu_data.get(\"options\", {}).get(\"title\", menu_key)\n            back_menu = menu_data.get(\"options\", {}).get(\"back\", \"Main menu\") or \"Main menu\"\n            buttons = []\n            for btn_key, btn_data in menu_data.items():\n                if btn_key.startswith(\"btn\"):\n                    if not isinstance(btn_data, dict):\n                        issues.append(f\"[FancyMenu] Button data for '{btn_key}' in menu '{menu_key}' is not a dictionary.\")\n                        continue\n                    title = btn_data.get(\"title\", f\"Button {btn_key[-1]}\")\n                    buttons.append((title, btn_data))\n            menus[menu_title] = Menu(menu_title, buttons, back_reference=back_menu)\n            self.menus.update(menus)\n        if issues:\n            logging.warning(\"[FancyMenu] Issues encountered during menu configuration: \\n\" + \"\\n\".join(issues))\n\n    def populate_plugins_menu(self,plugin_names):\n        menus = {}\n        sorted_plugin_names = sorted(plugin_names)\n        menus['Plugins toggle'] = Menu('Plugins toggle', [], back_reference='Plugins')\n        for plugin in sorted_plugin_names:\n            if plugin.lower() != 'fancygotchi':\n                if plugin != 'None' and plugin is not None:\n                    menus[plugin] = Menu(plugin, [\n                        (\"Enable plugin\", {\"action\": \"plugin\", \"name\": plugin, \"enable\": True}),\n                        (\"Disable plugin\", {\"action\": \"plugin\", \"name\": plugin, \"enable\": False}),\n                    ], back_reference='Plugins toggle')\n                    menus['Plugins toggle'].items.append((\n                        plugin.capitalize(), {\"action\": \"submenu\", \"name\": plugin}\n                    ), )\n        self.menus.update(menus)\n\n    def populate_themes_menu(self):\n        theme_names = self._fancygotchi.theme_list()\n        menus = {}\n        sorted_theme_names = sorted(theme_names)\n        menus['Theme selector'] = Menu('Theme selector', [], back_reference='Fancygotchi')\n        for theme in theme_names:\n            menus[theme] = Menu(theme, [\n                (f\"{theme} 0\", {\"action\": \"theme_select\", \"name\": theme, \"rotation\": 0,}),\n                (f\"{theme} 90\", {\"action\": \"theme_select\", \"name\": theme, \"rotation\": 90}),\n                (f\"{theme} 180\", {\"action\": \"theme_select\", \"name\": theme, \"rotation\": 180}),\n                (f\"{theme} 270\", {\"action\": \"theme_select\", \"name\": theme, \"rotation\": 270}),\n            ], back_reference='Theme selector')\n            menus['Theme selector'].items.append((\n                theme.capitalize(), {\"action\": \"submenu\", \"name\": theme}\n            ), )\n        self.menus.update(menus)\n\n    def toggle(self):\n        self.active = not self.active\n        self.last_activity_time = time.time() \n        return self.active\n\n    def navigate(self, direction):\n        if self.active:\n            current_menu = self.menu_stack[-1]\n            if direction in ['up', 'down']:\n                current_menu.navigate(direction)\n            elif direction == 'left':\n                if len(self.menu_stack) > 1:\n                    self.menu_stack.pop()\n            elif direction == 'right':\n                selected_item = current_menu.items[current_menu.current_index]\n                if isinstance(selected_item[1], dict) and selected_item[1].get('action') == 'submenu':\n                    submenu_name = selected_item[1]['name']\n                    if submenu_name in self.menus:\n                        self.menu_stack.append(self.menus[submenu_name])\n            self.last_activity_time = time.time()  \n    def select(self):\n        current_menu = self.menu_stack[-1]\n        return current_menu.items[current_menu.current_index][1]\n\n    def check_timeout(self):\n        if self.timeout != 0:\n            current_time = time.time()\n            if current_time - self.last_activity_time > self.timeout:\n                logging.debug(\"[FancyMenu] Session timed out.\")\n                self.active = False\n                return True\n            return False\n        else:\n            self.active = True\n            return False\n\n    def render(self):\n        try:\n            if self.active:\n                if self.check_timeout():\n                    return\n\n                if not hasattr(self, 'loaded_images'):\n                    self.loaded_images = {}\n\n                current_menu = self.menu_stack[-1]\n                rot = self._fancygotchi._config['main']['plugins']['Fancygotchi']['rotation']\n                if rot == 0 or rot == 180:\n                    canvas_width, canvas_height = self._fancygotchi._res\n                elif rot == 90 or rot == 270:\n                    canvas_width = self._fancygotchi._res[1]\n                    canvas_height = self._fancygotchi._res[0]\n\n                menu_width = self.menu_theme.get('width', 100)\n                menu_height = self.menu_theme.get('height', '100%')\n\n                menu_x, menu_y, menu_x2, menu_y2 = Fancygotchi.pos_convert(\n                    self._fancygotchi,\n                    self.menu_theme.get('position', [0, 0])[0],\n                    self.menu_theme.get('position', [0, 0])[1],\n                    menu_width,\n                    menu_height,\n                    r=0,\n                    r0=canvas_width,\n                    r1=canvas_height,\n                )\n\n                if self.menu_theme.get('bg_color', (0, 0, 0, 0)) == '': bg_color = (0,0,0,0)\n                else: bg_color = self.menu_theme.get('bg_color', (0, 0, 0, 0))\n                text_speed = self.menu_theme.get('motion_text_speed', 20)\n                menu_width = menu_x2 - menu_x\n                menu_height = menu_y2 - menu_y\n   \n                menu_image = Image.new(\"RGBA\", (menu_width, menu_height), bg_color)\n                draw = ImageDraw.Draw(menu_image)\n\n                draw.rectangle([0, 0, menu_width, menu_height], fill=bg_color)\n\n                bg_image_path = None\n                if self.menu_theme.get('bg_image', None):\n                    bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('bg_image'))\n                if bg_image_path:\n                    if bg_image_path not in self.loaded_images:\n                        if os.path.exists(bg_image_path):\n                            try:\n                                bg_image = Image.open(bg_image_path)\n                                self.loaded_images[bg_image_path] = bg_image\n                            except Exception as e:\n                                logging.warning(f\"Failed to load background image: {e}\")\n                                self.loaded_images[bg_image_path] = None \n                        else:\n                            logging.warning(f\"Background image not found: {bg_image_path}\")\n                            self.loaded_images[bg_image_path] = None\n                            \n                    if self.loaded_images[bg_image_path]:\n                        bg_mode = self.menu_theme.get('bg_mode', 'normal')\n                        bg_tmp = image_mode(menu_image, self.loaded_images[bg_image_path], bg_mode)\n\n                title_font_size = self.menu_theme.get('title_font_size', 'Medium')\n                title_font = getattr(self._fancygotchi, title_font_size)\n                title_color = self.menu_theme.get('title_color', 'black')\n\n                if title_font:\n                    title_text = current_menu.name\n                    try:\n                        title_width, title_height = draw.textsize(title_text, font=title_font)\n                    except:\n                        _, _, title_width, title_height = draw.textbbox((0,0),title_text, font=title_font)\n\n                    title_x, title_y, _, _ = Fancygotchi.pos_convert(\n                        self._fancygotchi,\n                        self.menu_theme.get('title_position', ['center', '5'])[0],\n                        self.menu_theme.get('title_position', ['center', '5'])[1],\n                        title_width,\n                        title_height,\n                        r=0,\n                        r0=menu_width,\n                        r1=menu_height,\n                    )\n                    try:\n                        title_box = draw.textsize(title_text, font=title_font)\n                        title_size = (title_box[0], title_box[1])\n                    except:\n                        title_box = draw.textbbox((0, 0), title_text, font=title_font)\n                        title_size = (title_box[2], title_box[3])\n                    if title_size[0] > menu_width and self.menu_theme.get('motion_text', True):\n                        self.scroll_text(draw, title_text, title_color, title_text, title_font, menu_width, text_speed)\n                    else:\n                        draw.text((title_x, title_y), title_text, font=title_font, fill=title_color)\n\n                btn_height = self.menu_theme.get('button_height', 15)\n                btns_width = self.menu_theme.get('buttons_width', '90%')\n                btns_height = self.menu_theme.get('buttons_height', '90%')\n                button_spacing = self.menu_theme.get('button_spacing', 5)\n\n                if isinstance(btns_width, str) and '%' in btns_width:\n                    base_width = menu_width\n                    btns_menu_width = int((base_width / 100) * int(btns_width.replace('%', '')))\n                else:\n                    btns_menu_width = int(btns_width)\n\n                if isinstance(btns_height, str) and '%' in btns_height:\n                    base_height = (menu_height - title_height - title_y)\n                    btns_menu_height = int((base_height / 100) * int(btns_height.replace('%', '')))\n                else:\n                    btns_menu_height = int(btns_height)\n\n                buttons_x, buttons_y, buttons_x1, buttons_y1 = Fancygotchi.pos_convert(\n                    self._fancygotchi,\n                    self.menu_theme.get('buttons_position', ['center', 'center'])[0],\n                    self.menu_theme.get('buttons_position', ['center', 'center'])[1],\n                    btns_width,\n                    btns_height,\n                    r=0,\n                    r0=menu_width,\n                    r1=menu_height,\n                )\n\n                button_font_size = self.menu_theme.get('button_font_size', 'Medium')\n                button_font = getattr(self._fancygotchi, button_font_size, None)\n\n                visible_buttons = (menu_height - title_height - title_y) // (btn_height + button_spacing)\n                scroll_offset = max(0, current_menu.current_index - visible_buttons + 1)\n\n                for i, (item_name, item_action) in enumerate(current_menu.items[scroll_offset:scroll_offset + visible_buttons]):\n                    button_y = title_height + title_y + i * (btn_height + button_spacing)\n\n                    if button_font:\n                        button_text = item_name\n                        try:\n                            text_width, text_height = draw.textsize(button_text, font=button_font)\n                        except:\n                            _, _, text_width, text_height = draw.textbbox((0, 0), button_text, font=button_font)\n                        \n\n                    text_x, text_y, _, _ = Fancygotchi.pos_convert(\n                        self._fancygotchi,\n                        self.menu_theme.get('text_position', ['center', '5'])[0],\n                        self.menu_theme.get('text_position', ['center', '5'])[1],\n                        text_width,\n                        text_height,\n                        r=0,\n                        r0=btns_menu_width,\n                        r1=btn_height,\n                    )\n\n                    button_image = Image.new(\"RGBA\", (btns_menu_width, btn_height), bg_color)\n                    \n                    button_draw = ImageDraw.Draw(button_image)\n                    if self.menu_theme.get('button_bg_color', (0,0,0,0)) == '': button_bg_color = (0,0,0,0)\n                    else: button_bg_color = self.menu_theme.get('button_bg_color', 'white')\n\n                    button_bg_image_path = None\n                    highlight_button_bg_image_path = None\n\n                    if self.menu_theme.get('button_bg_image', ''):\n                        button_bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('button_bg_image'))\n\n                    if self.menu_theme.get('highlight_button_bg_image', ''):\n                        highlight_button_bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('highlight_button_bg_image'))\n\n                    if button_bg_image_path and button_bg_image_path not in self.loaded_images:\n                        if os.path.exists(button_bg_image_path):\n                            try:\n                                button_bg_image = Image.open(button_bg_image_path)\n                                button_bg_image = button_bg_image.convert(\"RGBA\")\n                                try:\n                                    button_bg_image = button_bg_image.resize((btns_menu_width, btn_height), Image.ANTIALIAS)\n                                except:\n                                    button_bg_image = button_bg_image.resize((btns_menu_width, btn_height), Image.Resampling.LANCZOS)\n                                self.loaded_images[button_bg_image_path] = button_bg_image\n                            except Exception as e:\n                                logging.error(f\"[FancyMenu] Failed to load button background image: {e}\")\n                                self.loaded_images[button_bg_image_path] = None\n                        else:\n                            logging.warning(f\"Button background image not found: {button_bg_image_path}\")\n                            self.loaded_images[button_bg_image_path] = None\n\n                    if highlight_button_bg_image_path and highlight_button_bg_image_path not in self.loaded_images:\n                        if os.path.exists(highlight_button_bg_image_path):\n                            try:\n                                highlight_button_bg_image = Image.open(highlight_button_bg_image_path)\n                                highlight_button_bg_image = highlight_button_bg_image.convert(\"RGBA\")\n                                try:\n                                    highlight_button_bg_image = highlight_button_bg_image.resize((btns_menu_width, btn_height), Image.ANTIALIAS)\n                                except:\n                                    highlight_button_bg_image = highlight_button_bg_image.resize((btns_menu_width, btn_height), Image.Resampling.LANCZOS)\n                                self.loaded_images[highlight_button_bg_image_path] = highlight_button_bg_image\n                            except Exception as e:\n                                logging.error(f\"[FancyMenu] Failed to load highlight button background image: {e}\")\n                                self.loaded_images[highlight_button_bg_image_path] = None\n                        else:\n                            logging.warning(f\"Highlight button background image not found: {highlight_button_bg_image_path}\")\n                            self.loaded_images[highlight_button_bg_image_path] = None\n\n                    if i + scroll_offset == current_menu.current_index:\n                        highlight_color = self.menu_theme.get('highlight_color', 'black')\n                        highlight_text_color = self.menu_theme.get('highlight_text_color', 'white')\n                        \n                        button_draw.rectangle([0, 0, btns_menu_width, btn_height], fill=highlight_color)\n\n                        image_to_use_path = highlight_button_bg_image_path if self.loaded_images.get(highlight_button_bg_image_path) else button_bg_image_path\n                        if self.loaded_images.get(image_to_use_path):\n                            button_image.paste(self.loaded_images[image_to_use_path], (0, 0), self.loaded_images[image_to_use_path].split()[3])\n\n                        try:\n                            button_box = button_draw.textsize(button_text, font=button_font)\n                            button_size = (button_box[0], button_box[1])\n                        except:\n                            button_box = button_draw.textbbox((0, 0), button_text, font=button_font)\n                            button_size = (button_box[2], button_box[3])\n                        if button_size[0] > menu_width and self.menu_theme.get('motion_text', True):\n                            self.scroll_text(button_draw, button_text, highlight_text_color, button_text, button_font, menu_width, text_speed)\n                        else:\n                            button_draw.text((text_x, text_y), button_text, font=button_font, fill=highlight_text_color)\n\n                    else:\n                        button_text_color = self.menu_theme.get('button_text_color', 'black')\n                        button_draw.rectangle([0, 0, btns_menu_width, btn_height], fill=button_bg_color)\n\n                        if self.loaded_images.get(button_bg_image_path):\n                            button_image.paste(self.loaded_images[button_bg_image_path], (0, 0), self.loaded_images[button_bg_image_path].split()[3])\n\n                        try:\n                            button_box = button_draw.textsize(button_text, font=button_font)\n                            button_size = (button_box[0], button_box[1])\n                        except:\n                            button_box = button_draw.textbbox((0, 0), button_text, font=button_font)\n                            button_size = (button_box[2], button_box[3])\n                        if button_size[0] > menu_width and self.menu_theme.get('motion_text', True):\n                            self.scroll_text(button_draw, button_text, button_text_color, button_text, button_font, menu_width, text_speed)\n                        else:\n                            button_draw.text((text_x, text_y), button_text, font=button_font, fill=button_text_color)\n                    menu_image.paste(button_image, (buttons_x, button_y), button_image.split()[3])\n\n                    draw.rectangle([0, 0, menu_width - 1, menu_height - 1], outline=self.menu_theme.get('border_color', 'black'))\n\n                    canvas = Image.new(\"RGBA\", (canvas_width, canvas_height), (0, 0, 0, 0))\n                    canvas.paste(menu_image, (menu_x, menu_y))\n\n                return canvas\n\n        except Exception as e:\n            logging.error(f\"Failed to render menu: {e}\")\n            logging.error(traceback.format_exc())\n\n    def scroll_text(self, draw, menu_item_key, color, scrolltext, scrollfont, menu_width, distance=10):\n        scroll_state = self.scroll_state.get(menu_item_key, None)\n        if not scroll_state:\n            try:\n                text_width, text_height = draw.textsize(scrolltext, font=scrollfont)\n            except:\n                _, _, text_width, text_height = draw.textbbox((0, 0), scrolltext, font=scrollfont)\n            \n            scroll_state = {\n                'text_width': text_width,\n                'position': 10 \n            }\n            self.scroll_state[menu_item_key] = scroll_state\n\n        text_width = scroll_state['text_width']\n        text_position = scroll_state['position']\n        draw.text((text_position, 0), scrolltext, font=scrollfont, fill=color)\n\n        if text_position + text_width < menu_width:\n            draw.text((text_position + text_width, 0), f' - {scrolltext}', font=scrollfont, fill=color)\n\n        text_position -= distance\n\n        if text_position + text_width <= 0:\n            text_position += text_width\n\n        self.scroll_state[menu_item_key]['position'] = text_position\n\nclass Menu:\n    def __init__(self, name, items, back_reference=\"Main menu\"):\n        self.name = name\n        self.back_reference = back_reference\n        self.current_index = 0\n\n        if not name == 'Main menu':\n            if self.back_reference == \"Main menu\":\n                self.items = [\n                    (\"Home\", {\"action\": \"submenu\", \"name\": \"Main menu\"})\n                ] + items\n            else:\n                self.items = [\n                    (\"Back\", {\"action\": \"submenu\", \"name\": back_reference}),\n                    (\"Home\", {\"action\": \"submenu\", \"name\": \"Main menu\"})\n                ] + items\n        else:\n            self.items = items\n\n    def navigate(self, direction):\n        if direction in ['up', 'down']:\n            self.current_index = (self.current_index + (1 if direction == 'down' else -1)) % len(self.items)\n\n    def add_button(self, title, action):\n        self.items.insert(0, (title, action))  \n\ndef menu_contains_button(menu, button_name):\n    for item in menu.items:\n        if item[0] == button_name:\n            return True\n    return False\n\nMENUS = {\n    'Main menu': Menu('Main menu', [\n        (\"Plugins\", {\"action\": \"submenu\", \"name\": \"Plugins\"}),\n        (\"Fancygotchi\",{\"action\": \"submenu\", \"name\": \"Fancygotchi\"}),\n        (\"System\", {\"action\": \"submenu\", \"name\": \"System\"}),\n    ]),\n    'System': Menu('System', [\n        (\"Restart Auto\", {\"action\": \"restart\", \"mode\": \"auto\"}),\n        (\"Restart Manu\", {\"action\": \"restart\", \"mode\": \"manu\"}),\n        (\"Reboot Auto\", {\"action\": \"reboot\", \"mode\": \"auto\"}),\n        (\"Reboot Manu\", {\"action\": \"reboot\", \"mode\": \"manu\"}),\n        (\"Shutdown\", {\"action\": \"shutdown\"}),\n    ]),\n    'Fancygotchi': Menu('Fancygotchi', [\n        (\"Theme selector\", {\"action\": \"submenu\", \"name\": \"Theme selector\"}),\n        (\"Second screen\", {\"action\": \"submenu\", \"name\": \"Second screen\"}),\n        (\"Theme refresh\", {\"action\": \"theme_refresh\"}),\n        (\"Stealth mode\", {\"action\": \"stealth_mode\"}),\n    ]),\n    'Plugins': Menu('Plugins', [\n        (\"Refresh plugins\", {\"action\": \"refresh_plugins\"}),\n        (\"Plugins toggle\", {\"action\": \"submenu\", \"name\": \"Plugins toggle\"}),\n    ]),\n    'Second screen': Menu('Second screen', [\n        ('Activate second screen', {'action': 'enable_second_screen'}),\n        ('Switch screen mode', {'action': 'switch_screen_mode'}),\n        ('Switch screen saver mode', {'action': 'switch_screen_saver'}),\n    ]),\n}\n\ndef check_internet_and_repo():\n    try:\n        requests.get(\"https://www.google.com\", timeout=5)\n        response = requests.get(THEMES_REPO, timeout=5)\n        \n        if response.status_code == 200:\n            return True, \"Connection successful\"\n        else:\n            error_msg = f\"Repository not accessible. Status code: {response.status_code}\"\n            logging.warning(error_msg)\n            return False, error_msg\n            \n    except requests.ConnectionError as e:\n        error_msg = f\"No internet connection: {str(e)}\"\n        logging.warning(error_msg)\n        return False, error_msg\n        \n    except requests.Timeout as e:\n        error_msg = f\"Connection timed out: {str(e)}\"\n        logging.warning(error_msg)\n        return False, error_msg\n\ndef get_all_plugin_names(fancygotchi):\n    config_dict = fancygotchi._config \n    plugins = list(config_dict['main'].get('plugins', {}).keys())\n    custom_plugins_path = config_dict['main'].get('custom_plugins', '')\n    all_plugins = plugins\n    return all_plugins\n\ndef is_int(s):\n    try:\n        int(s)\n        return True\n    except ValueError:\n        return False\n\ndef box_to_xywh(position):\n    dist_1 = math.sqrt(position[0]**2 + position[1]**2)\n    dist_2 = math.sqrt(position[2]**2 + position[3]**2)\n    \n    if dist_1 <= dist_2:\n        x, y = position[0], position[1]\n        x2, y2 = position[2], position[3]\n    else:\n        x, y = position[2], position[3]\n        x2, y2 = position[0], position[1]\n\n    w = abs(x - x2)\n    h = abs(y - y2)\n    \n    return [x, y, w, h]\n\ndef adjust_image(image_path, zoom, mask=False, refine=150, alpha=False, invert=False, crop=[0,0,0,0]):\n    try:\n        if isinstance(image_path, str):\n            try:\n                image = Image.open(image_path)\n            except Exception as e:\n                logging.error(f\"Error opening image: {e}\")\n                return None\n        elif isinstance(image_path, Image.Image):\n            image = image_path\n        if invert:\n            image = invert_pixels(image)\n        if crop != [0,0,0,0]:\n            image = image.crop(crop)\n        image = image.convert('RGBA') \n        \n        original_width, original_height = image.size\n        new_width = int(original_width * zoom)\n        new_height = int(original_height * zoom)\n\n        adjusted_image = image.resize((new_width, new_height))\n        if mask:\n            new_img = adjusted_image\n            adjusted_image = masking(new_img, refine)\n        \n        if alpha: \n            adjusted_image = alphamask(adjusted_image)\n        \n        return adjusted_image\n    except Exception as e:\n        logging.error(\"Error:\", str(e))\n        return None\n    \ndef invert_pixels(image):\n    try:\n        image = image.convert('RGBA')\n        data = list(image.getdata())\n        inverted_data = [(255-r, 255-g, 255-b, a) for r, g, b, a in data]\n        inverted_image = Image.new('RGBA', image.size)\n        inverted_image.putdata(inverted_data)\n        return inverted_image\n    except Exception as e:\n        logging.error(f\"Error in invert_pixels: {str(e)}\")\n        logging.error(traceback.format_exc())\n        return image  \n\ndef alphamask(src_image):\n    src_image = src_image.convert('RGBA')\n    data = src_image.getdata()\n    newData = []\n    for item in data:\n        if item[0] in range(240, 256) and item[1] in range(240, 256) and item[2] in range(240, 256):\n            newData.append((255, 255, 255, 0))\n        else:\n            newData.append(item)\n    src_image.putdata(newData)\n    src_image = src_image.convert('RGBA')\n    return src_image\n\ndef masking(src_image, refine):\n    image = src_image.convert('RGBA') \n    width, height = image.size\n    pixels = image.getdata()\n    new_pixels = []\n    for pixel in pixels:\n        r, g, b, a = pixel\n        if a > refine:\n            new_pixel = (0, 0, 0, 255)\n        else:\n            new_pixel = (0, 0, 0, 0)\n        new_pixels.append(new_pixel)\n    new_img = Image.new(\"RGBA\", image.size)\n    new_img.putdata(new_pixels)\n    adjusted_image = new_img\n    return adjusted_image\n\ndef image_mode(canvas, image, mode):\n    w, h = canvas.size\n    width, height = image.size\n    logging.debug(f\"Mode: {mode}\")\n    logging.debug(f\"Image size: {width}x{height}\")\n    logging.debug(f\"Canvas size: {w}x{h}\")\n    if mode == 'normal':\n        image = image.convert('RGBA')\n        canvas.paste(image, (0,0,width, height), image.split()[3])\n    elif mode == 'stretch':\n        img_resized = image.resize((w,h))\n        canvas.paste(img_resized, (0, 0), img_resized)\n    elif mode == 'tile':\n        for x in range(0, w, image.width):\n            for y in range(0, h, image.height):\n                canvas.paste(image, (x, y), image)\n    elif mode == 'center':\n        x = (w - image.width) // 2\n        y = (h - image.height) // 2\n        canvas.paste(image, (x, y), image)\n    elif mode == 'fit':\n        original_width, original_height = image.size\n        canvas_width, canvas_height = canvas.size\n\n        original_aspect = original_width / original_height\n        canvas_aspect = canvas_width / canvas_height\n\n        if original_aspect > canvas_aspect:\n            new_width = canvas_width\n            new_height = int(canvas_width / original_aspect)\n        else:\n            new_height = canvas_height\n            new_width = int(canvas_height * original_aspect)\n\n        try:\n            image_resized = image.resize((new_width, new_height), Image.ANTIALIAS)\n        except:\n            image_resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)\n\n        x = (canvas_width - new_width) // 2\n        y = (canvas_height - new_height) // 2\n\n        canvas.paste(image_resized, (x, y), image_resized) \n\n    elif mode == 'fill':\n        img_resized = ImageOps.fit(image, (w,h))\n        canvas.paste(img_resized, (0, 0), img_resized)\n    return canvas\n\ndef verify_font_info(ft):\n    font_list = [fonts.Bold, fonts.BoldSmall, fonts.Medium, fonts.Huge, fonts.BoldBig, fonts.Small]\n\n    font_info = {\n        'Bold': fonts.Bold,\n        'BoldSmall': fonts.BoldSmall,\n        'Medium': fonts.Medium,\n        'Huge': fonts.Huge,\n        'BoldBig': fonts.BoldBig,\n        'Small': fonts.Small\n    }\n\n    for font in font_info:\n        if font_info[font].size == ft.size and font_info[font].getname() == ft.getname():\n            return font\n    return ft\n\ndef allowed_file(filename):\n    allowed_ext = {'zip'}\n    return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_ext\n\ndef unzip_file(zip_file, extract_to):\n    with zipfile.ZipFile(zip_file, 'r') as zip_ref:\n        zip_ref.extractall(extract_to)\n    os.remove(zip_file)\n\ndef serializer(obj):\n    if isinstance(obj, set):\n        return list(obj)\n    raise TypeError\n\ndef _compile_po_to_mo(po_file_path):\n    \"\"\"\n    Compiles a .po file to a .mo file in memory.\n    This is a lightweight, pure-Python implementation based on the standard\n    `msgfmt.py` tool.\n    \"\"\"\n    try:\n        with open(po_file_path, 'r', encoding='utf-8') as f:\n            lines = f.readlines()\n\n        messages = {}\n        msgid = \"\"\n        msgstr = \"\"\n        is_fuzzy = False\n        in_msgid = False\n        in_msgstr = False\n\n        for line in lines:\n            line = line.strip()\n            if not line:\n                if msgid and not is_fuzzy:\n                    messages[msgid] = msgstr\n                msgid, msgstr, is_fuzzy = \"\", \"\", False\n                in_msgid, in_msgstr = False, False\n            elif line.startswith('#,') and 'fuzzy' in line:\n                is_fuzzy = True\n            elif line.startswith('msgid '):\n                in_msgid, in_msgstr = True, False\n                msgid = line[6:].strip('\"')\n            elif line.startswith('msgstr '):\n                in_msgid, in_msgstr = False, True\n                msgstr = line[7:].strip('\"')\n            elif line.startswith('\"'):\n                if in_msgid:\n                    msgid += line.strip('\"')\n                elif in_msgstr:\n                    msgstr += line.strip('\"')\n\n        if msgid and not is_fuzzy:\n            messages[msgid] = msgstr\n\n        # Build the .mo file format in memory\n        magic = 0x950412de\n        revision = 0\n        num_strings = len(messages)\n        \n        # Sort by msgid\n        sorted_messages = sorted(messages.items())\n\n        # Create string tables\n        orig_table = b''\n        trans_table = b''\n        for msgid, msgstr in sorted_messages:\n            orig_table += msgid.encode('utf-8') + b'\\0'\n            trans_table += msgstr.encode('utf-8') + b'\\0'\n\n        # Calculate offsets\n        header_size = 7 * 4\n        orig_offset_table_offset = header_size\n        trans_offset_table_offset = orig_offset_table_offset + num_strings * 8\n        strings_offset = trans_offset_table_offset + num_strings * 8\n\n        output = bytearray(struct.pack('<IIIIIII', magic, revision, num_strings, orig_offset_table_offset, trans_offset_table_offset, 0, 0))\n        \n        orig_addr = strings_offset\n        trans_addr = strings_offset + len(orig_table)\n        for msgid, msgstr in sorted_messages:\n            output.extend(struct.pack('<II', len(msgid), orig_addr))\n            orig_addr += len(msgid) + 1\n        for msgid, msgstr in sorted_messages:\n            output.extend(struct.pack('<II', len(msgstr), trans_addr))\n            trans_addr += len(msgstr) + 1\n\n        output.extend(orig_table)\n        output.extend(trans_table)\n        return bytes(output)\n\n    except Exception as e:\n        logging.error(f\"[Fancygotchi] Error compiling .po to .mo: {e}\", exc_info=True)\n        return None\n\nclass Fancygotchi(plugins.Plugin):\n    __author__ = 'V0rT3x'\n    __github__ = 'https://github.com/V0r-T3x/Fancygotchi'\n    __version__ = '2.0.8'\n    __license__ = 'GPL3'\n    __description__ = 'The Ultimate theme manager for pwnagotchi'\n\n    def __init__(self):\n        self.pyenv = sys.executable    \n        self.running = False\n        self.fancy_menu = None\n        self.actions_log = []\n        self.second_screen = Image.new('RGBA', (1,1), 'black')\n        self.fancy_menu_img = None\n        self.display_config = {'mode': 'screen_saver', 'sub_mode': 'show_logo'}\n        self.screen_modes = ['screen_saver', 'auxiliary', 'terminal']\n        self.screen_saver_modes = ['show_logo', 'moving_shapes', 'random_colors', 'hyper_drive', 'show_animation']\n        self.dispHijack = False\n        self.loop = None\n        self.refacer_thread = None\n        self._stop_event = threading.Event()\n\n        self.bitmap_widget = ('Bitmap', 'WardriverIcon', 'InetIcon', 'Frame', 'WifiQR')\n        self._config = pwnagotchi.config\n        self.gittoken = self._config['main']['plugins']['Fancygotchi'].get('github_token', None)\n        self.cfg_path = None\n        self.cursor_list = ['█', '-']\n        self.options = dict()\n        self._agent = None\n        self.ready = False\n        self.stealth_mode = False\n        self.refresh = False\n        self.refresh_menu = False\n        self.last_cmd = None\n        self.refresh_trigger = -1\n        self.star = '*'\n        logging.info(f'[Fancygotchi]{20*self.star}[Fancygotchi]{20*self.star}')\n        self._pwny_root = os.path.dirname(pwnagotchi.__file__)\n        self._plug_root = os.path.dirname(os.path.realpath(__file__))\n        self.orientation = 'horizontal'\n        self._default = {\n            'theme': {\n                'options': {\n                    'boot_animation': False,\n                    'boot_mode': 'normal', # Implementation to adjust the boot animation image with normal, stretch, fit, fill, center or tile\n                    'boot_max_loops': 1,\n                    'boot_total_duration': 1,\n                    'screen_mode': 'screen_saver',\n                    'screen_saver': 'show_logo',\n                    'second_screen_fps': 1,\n                    'webui_fps': 1,\n                    'second_screen_webui': True,\n                    'bg_fg_select': 'manu',\n                    'bg_mode': 'normal',\n                    'fg_mode': 'normal',\n                    'fg_image': '',\n                    'bg_color': 'white',\n                    'bg_image': '',\n                    'bg_anim_image': '',\n                    #[Bold, BoldSmall, Medium, Huge, BoldBig, Small]\n                    'font_sizes': [14, 9, 14, 25, 19, 9],\n                    'font': 'DejaVuSansMono',\n                    'font_bold': 'DejaVuSansMono-Bold',\n                    'status_font': 'DejaVuSansMono',\n                    'font_awesome': '',\n                    'size_offset': 5,\n                    'label_spacing': 9,\n                    'label_line_spacing': 0,\n                    'cursor': '❤',\n                    'friend_bars': '▌',\n                    'friend_no_bars': '│',\n                    'base_text_color': ['black'],\n                    'main_text_color': ['black'],\n                    'color_mode': ['P', 'P'],\n                    'faces': {\n                        'look_r': \"( ⚆_⚆)\",\n                        'look_l': \"(☉_☉ )\",\n                        'look_r_happy': \"( ◕‿◕)\",\n                        'look_l_happy': \"(◕‿◕ )\",\n                        'sleep': \"(⇀‿‿↼)\",\n                        'sleep2': \"(≖‿‿≖)\",\n                        'awake': \"(◕‿‿◕)\",\n                        'bored': \"(-__-)\",\n                        'intense': \"(°▃▃°)\",\n                        'cool': \"(⌐■_■)\",\n                        'happy': \"(•‿‿•)\",\n                        'excited': \"(ᵔ◡◡ᵔ)\",\n                        'grateful': \"(^‿‿^)\",\n                        'motivated': \"(☼‿‿☼)\",\n                        'demotivated': \"(≖__≖)\",\n                        'smart': \"(✜‿‿✜)\",\n                        'lonely': \"(ب__ب)\",\n                        'sad': \"(╥☁╥ )\",\n                        'angry': \"(-_-')\",\n                        'friend': \"(♥‿‿♥)\",\n                        'broken': \"(☓‿‿☓)\",\n                        'debug': \"(#__#)\",\n                        'upload': \"(1__0)\",\n                        'upload1': \"(1__1)\",\n                        'upload2': \"(0__1)\",\n                    }\n                },\n                'widget': {}\n            }\n        }\n        self._default_menu = {\n            'motion_text': True,\n            'motion_text_speed': 20,\n\n            'bg_select': 'manu',\n            'bg_mode': 'normal',\n\n            'position': [0, 0],\n            'title_position': ['center', '5'],\n            'title_font_size': 'Medium',\n            'title_color': 'black',\n            'width': 100,\n            'height': '100%',\n\n            'buttons_position': ['center', '5'],\n            'buttons_width': '90%',\n            'button_height': 15,\n            'button_spacing': 3,\n\n            'bg_color': 'white',\n            'border_color': 'black',\n            'highlight_color': 'black',\n            'highlight_border_color': 'white',\n            'highlight_text_color': 'white',\n            'button_bg_color': 'white',\n            'button_bg_border_color': 'black',\n            'button_text_color': 'black',\n\n            'bg_image': \"\",\n            'button_bg_image': \"\",\n            'highlight_button_bg_image': \"\",\n\n            'text_position': ['center', 'center'],\n            'button_font_size': \"Medium\",\n            'timeout': 30,\n        }\n        text_widget_defaults = {\n            'position': [0, 0],\n            'color': ['#000000'],\n            'z_axis': 0,\n            'text_font': '',\n            'text_font_size': \"Medium\",\n            'size_offset': 0,\n            'icon': False,\n            'icon_color': False,\n            'invert': False,\n            'alpha': False,\n            'crop': [0,0,0,0],\n            'mask': False,\n            'refine': 150,\n            'zoom': 1,\n            'image_type': 'png',\n            'wrap': False,\n            'max_length': 0\n        }\n        labeledvalue_widget_defaults = {\n            'position': [0, 0],\n            'color': ['#000000'],\n            'z_axis': 0,\n            'text_font': '',\n            'text_font_size': \"Medium\",\n            'size_offset': 0,\n            'icon': False,\n            'icon_color': False,\n            'invert': False,\n            'alpha': False,\n            'crop': [0,0,0,0],\n            'mask': False,\n            'refine': 150,\n            'zoom': 1,\n            'label': '',\n            'label_font': '',\n            'label_font_size': \"Medium\",\n            'label_spacing': 0,\n            'label_line_spacing': 0,\n            'f_awesome': False,\n            'f_awesome_size': 0\n        }\n        line_widget_defaults = {\n            'position': [0, 0, 0, 0],\n            'color': ['#000000'],\n            'z_axis': 0,\n            'width': 1\n        }\n        rect_widget_defaults = {\n            'position': [0, 0, 0, 0],\n            'color': ['#000000'],\n            'z_axis': 0\n        }\n        filledrect_widget_defaults = {\n            'position': [0, 0, 0, 0],\n            'color': ['#000000'],\n            'z_axis': 0\n        }\n        bitmap_widget_defaults = {\n            'position': [0, 0],\n            'color': ['#000000'],\n            'z_axis': 0,\n            'icon': False,\n            'invert': False,\n            'alpha': False,\n            'crop': [0,0,0,0],\n            'mask': False,\n            'refine': 150,\n            'zoom': 1,\n            'icon_color': False,\n        }\n        self.widget_defaults = {\n            'Text': text_widget_defaults,\n            'LabeledValue': labeledvalue_widget_defaults,\n            'Line': line_widget_defaults,\n            'Rect': rect_widget_defaults,\n            'FilledRect': filledrect_widget_defaults,\n            'Bitmap': bitmap_widget_defaults\n        }\n        \n        self._theme_name = 'Default'\n        self._theme = copy.deepcopy(self._default)\n        self._th_path = None\n        self._res = []\n        self._color_mode = ['P', 'P']\n        self._bg = ''\n        self._fg = ''\n        self._i = 0\n        self._imax = None\n        self._frames = []\n        self._icolor = 0\n        self.font_name = 'DejaVuSansMono'\n        self.font_bold_name = 'DejaVuSansMono-Bold'\n        self.f_awesome_name = ''\n        self.Bold = None\n        self.BoldSmall = None\n        self.BoldBig = None\n        self.Medium = None\n        self.Small = None\n        self.Huge = None\n        self._state = {}\n        self._state_default = {}\n\n        self.Tag = '# Pwned by V0rT3x'\n        v_code = [{'replace': False, \n                    'reference': 'lv.draw(self._canvas, drawer)',\n                    'paste': \"\"\"\n                # Start of the Fancygotchi hack\n                if hasattr(self, '_pwncanvas'):\n                    rot = pwnagotchi.config['main']['plugins']['Fancygotchi']['rotation']\n                    if self._pwncanvas_tmp is not None:\n                        self._pwncanvas = self._pwncanvas_tmp\n                        self._pwncanvas_tmp = None\n                    if self._pwncanvas is not None:\n                        if isinstance(self._pwncanvas, Image.Image):\n                            self._canvas = self._canvas.convert('RGBA')\n                            self._canvas.paste(self._pwncanvas, (0, 0), self._pwncanvas)\n                    web_tmp = self._canvas\n                    hw_tmp = self._canvas\n                    if rot in [90,270]: hw_tmp = hw_tmp.rotate(-90, expand=True)\n                    self._canvas = hw_tmp.convert(self._web_mode)# End of the Fancygotchi hack\"\"\"},\n                    {'replace': False, 'reference': 'web.update_frame(self._canvas)',\n                    'paste':\"\"\"                # Start of the Fancygotchi hack\n                if hasattr(self, '_pwncanvas'):\n                    self._canvas = hw_tmp.convert(self._hw_mode)\n                    if rot == 90: self._canvas = self._canvas.rotate(90, expand=True)\n                    if rot == 270: self._canvas = self._canvas.rotate(-90, expand=True)\n                    if rot == 180: self._canvas = self._canvas.rotate(180) # End of the Fancygotchi hack\"\"\"}]\n        s_code = [{'replace': False, \n                    'reference': 'self._listeners[key](prev, value)',\n                    'paste': \"\"\"\n    # Start of the Fancygotchi hack                    \n    def get_attr(self, key, attribute='value'):\n        with self._lock:\n            if key in self._state:\n                return getattr(self._state[key], attribute)\n            else:\n                return None\n    # End of the Fancygotchi hack\n    \"\"\"}]\n        p_code  = [{'replace': False, \n                'reference': 'source /usr/bin/pwnlib',\n                'paste': f\"\"\"\n# Start of the Fancygotchi hack\nif [ -f \"/usr/local/bin/boot_animation.py\" ]; then\n    {self.pyenv} /usr/local/bin/boot_animation.py\nfi # End of the Fancygotchi hack\"\"\"}]\n        v_f = os.path.join(self._pwny_root, 'ui', 'view.py')\n        s_f = os.path.join(self._pwny_root, 'ui', 'state.py')\n        p_f = '/usr/bin/pwnagotchi-launcher'\n        rst = 0\n        if self.adjust_code(v_f, v_code): rst = 1\n        if self.adjust_code(s_f, s_code): rst = 1\n        if self.adjust_code(p_f, p_code): rst = 1\n        if self.zram_check(): rst = 1\n        if self.fps_check(): rst = 1\n        self.check_and_fix_fb()\n        if rst:\n            self.log('The pwnagotchi need to restart.')\n            os.system('sudo systemctl restart pwnagotchi.service')\n            os.system('sudo service pwnagotchi restart')\n        self.log('Initiated')\n\n    def adjust_code(self, file_path, changes):\n        self.log(f'Adjusting code in {file_path}')\n        rst = 0\n        with open(file_path, 'r') as file:\n            lines = file.readlines()\n\n        for code in changes:\n            replace_flag = code.get('replace', False)\n            reference_lines = code.get('reference', '').split('\\n')\n            paste_code = code.get('paste', '')\n\n            if not lines[-1].strip() == self.Tag:\n                reference_index = 0\n                for i, line in enumerate(lines):\n                    if reference_index < len(reference_lines) and reference_lines[reference_index] in line:\n                        reference_index += 1\n                    else:\n                        reference_index = 0\n                    if reference_index == len(reference_lines):\n                        if replace_flag:\n                            lines[i - len(reference_lines) + 1:i + 1] = [paste_code + '\\n']\n                        else:\n                            lines[i] = lines[i].rstrip() + '\\n' + paste_code + '\\n'\n                        rst = 1\n        if rst:\n            lines.append(self.Tag + '\\n')\n        with open(file_path, 'w') as file:\n            file.writelines(lines)\n        \n        return rst\n\n    def check_and_fix_fb(self):\n        config_paths = [\n            \"/boot/firmware/config.txt\",\n            \"/boot/config.txt\"\n        ]\n        correct_overlay = \"dtoverlay=vc4-fkms-v3d\"\n        wrong_overlay = \"dtoverlay=vc4-kms-v3d\"\n\n        fb_device_exists = any(os.path.exists(f\"/dev/fb{i}\") for i in range(10))\n        self.log(f\"Framebuffer device exists: {fb_device_exists}\")\n        config_file = None\n        for path in config_paths:\n            if os.path.exists(path):\n                config_file = path\n                break\n\n        if not config_file:\n            return\n\n        with open(config_file, 'r') as file:\n            lines = file.readlines()\n\n        found_correct_overlay = any(correct_overlay in line for line in lines)\n\n        if fb_device_exists:\n            self.log(\"Framebuffer device exists. No reboot needed.\")\n            return\n        elif found_correct_overlay:\n            self.log(\"config.txt already contains the correct overlay. No reboot needed.\")\n            return\n        else:\n            self.log(\"Framebuffer device does not exist config.txt already don't contain the correct overlay. Rebooting system to apply changes...\")\n\n        backup_path = config_file + \".bak\"\n        shutil.copy(config_file, backup_path)\n        with open(config_file, 'r') as file:\n            lines = file.readlines()\n        found_wrong_overlay = False\n        found_correct_overlay = False\n        new_lines = []\n        for line in lines:\n            if wrong_overlay in line:\n                found_wrong_overlay = True\n                new_lines.append(line.replace(wrong_overlay, correct_overlay))\n            elif correct_overlay in line:\n                found_correct_overlay = True\n                new_lines.append(line)\n            else:\n                new_lines.append(line)\n        if not found_correct_overlay:\n            new_lines.append(f\"\\n{correct_overlay}\\n\")\n            self.log(f\"{correct_overlay} added to {config_file}\")\n        with open(config_file, 'w') as file:\n            file.writelines(new_lines)\n        self.log(\"Rebooting system to apply changes...\")\n        subprocess.run([\"sudo\", \"reboot\"])\n\n    def zram_check(self):\n        rst = 0\n        if 'fs' in self._config and 'memory' in self._config['fs'] and 'mounts' in self._config['fs']['memory'] and 'data' in self._config['fs']['memory']['mounts']:\n            fs_data = self._config['fs']['memory']['mounts']['data']\n            if 'enabled' in fs_data and fs_data['enabled']:\n                if 'mount' != '': mount = fs_data['mount']\n                else: self._config['fs']['memory']['mounts']['data']['mount'] = \"/var/tmp/pwnagotchi\"\n                if 'zram' in fs_data and fs_data['zram']:\n                    if 'size' in fs_data:\n                        size = num_size = int(re.search(r'\\d+', fs_data['size']).group())\n                        if num_size < 50:\n                            self._config['fs']['memory']['mounts']['data']['size'] = '250M' \n                            save_config(self._config, '/etc/pwnagotchi/config.toml')\n                            rst= 1\n        return rst\n\n    def fps_check(self):\n        rst = 0\n        if 'ui' in self._config and 'fps' in self._config['ui']:\n            fps_value = int(self._config['ui']['fps'])\n            if fps_value == 0:\n                self._config['ui']['fps'] = 1\n                save_config(self._config, '/etc/pwnagotchi/config.toml')\n                rst = 1\n        return rst\n\n    def log(self, msg):\n        try:\n            # working state\n            # log = False\n            # debug = True\n            log = False\n            debug = True\n\n            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'log' in self._theme['theme']['dev']:\n                log = self._theme['theme']['dev']['log']\n\n            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'debug' in self._theme['theme']['dev']:\n                debug = self._theme['theme']['dev']['debug']\n\n            if log:\n                if debug: logging.debug(msg)\n                else: logging.info(f'[Fancygotchi] {msg}')\n        except Exception as ex:\n            logging.error(ex)\n\n    def on_ready(self, agent):\n        self._agent = agent\n        self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'\n\n    def on_loaded(self):\n        logging.info(\"[Fancygotchi] Loaded\")\n        self.ready = True\n\n    def on_unload(self, ui):\n        with open('/etc/pwnagotchi/config.toml', 'r') as f:\n            f_toml = toml.load(f)\n            faces.load_from_config(f_toml['ui']['faces'])\n        with ui._lock:\n            self.cleanup_display()\n            self.dispHijack = False\n            if not self.dispHijack:\n                if hasattr(self, 'display_controller') and self.display_controller:\n                    self.display_controller.stop()\n                if hasattr(ui, '_enabled') and not ui._enabled:\n                    ui._enabled = True\n                    self.log(\"Switched back to the original display.\")\n        if self._config['ui']['display']['enabled']:\n            ui._enabled = True\n            ui.init_display()\n            # Start of Fancygotchi unload voice modification\n            try:\n                locale_path = os.path.join(self._pwny_root, 'locale', 'fancyvoice')\n                if os.path.islink(locale_path):\n                    os.unlink(locale_path)\n                    self.log(\"Removed fancyvoice symlink.\")\n                \n                # Reload the voice with the original system language\n                if hasattr(ui, '_config'):\n                    original_lang = ui._config['main']['lang']\n                    self.reload_voice(ui, lang=original_lang)\n            except Exception as e:\n                logging.error(f\"[Fancygotchi] Error during unload cleanup: {e}\")\n            # End of Fancygotchi unload voice modification\n\n            self.cleanup_display()\n        if hasattr(self, 'fancy_menu'):\n            del self.fancy_menu\n        if hasattr(self, 'listener'):\n            self.listener.close()\n        if hasattr(ui, '_pwncanvas'):\n            # Start of Fancygotchi unload voice modification\n            del ui._pwncanvas\n        if hasattr(ui, '_pwncanvas_tmp'):\n            del ui._pwncanvas_tmp\n        if hasattr(ui, '_update'):\n            del ui._update\n        if hasattr(ui, '_web_mode'):\n            del ui._web_mode\n        if hasattr(ui, '_hw_mode'):\n            del ui._hw_mode\n        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/screenshots')\n        if os.path.exists(screenshots_path):\n            os.system(f'rm -r {screenshots_path}')\n        repo_screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')\n        if os.path.exists(repo_screenshots_path):\n            os.system(f'rm -r {repo_screenshots_path}')\n        css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')\n        css_backup = css_dst + '.backup'\n        if os.path.exists(css_backup):\n            copyfile(css_backup, css_dst)\n            os.remove(css_backup)\n        img_dst = os.path.join(self._pwny_root, 'ui/web/static/img')\n        if os.path.islink(img_dst):\n            os.unlink(img_dst)\n        font_dst = '/usr/share/fonts/truetype/theme_fonts'\n        os.system('rm %s' % (font_dst))\n        icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')\n        icon_bkup = icon_dst + '.backup'\n        if os.path.exists(icon_bkup):\n            copyfile(icon_bkup, icon_dst)\n            os.remove(icon_bkup)\n        fancytools_path = \"/usr/local/bin/fancytools\"\n        if os.path.exists(fancytools_path):\n            os.remove(fancytools_path)\n        diagnostic_path = \"/usr/local/bin/diagnostic.sh\"\n        if os.path.exists(diagnostic_path):\n            os.remove(diagnostic_path)\n\n        logging.info('[Fancygotchi] Unloaded')\n\n    def on_ui_setup(self, ui):\n        logging.info('[Fancygotchi] UI setup start')\n        setattr(ui, '_pwncanvas_tmp', None)\n        setattr(ui, '_pwncanvas', None)\n        setattr(ui, '_web_mode', self._color_mode[0])\n        setattr(ui, '_hw_mode', self._color_mode[1])\n        setattr(ui, '_update', {\n            'update': True,\n            'partial': False,\n            'dict_part': {}\n        })\n        self.log(f\"UI attributes created: {ui._update}, {ui._web_mode}, {ui._hw_mode}\")\n        self._res = [ui._width, ui._height]\n        self.log(f\"UI resolution: {self._res}\")\n        self.theme_update(ui, True)\n        self.pwncanvas_creation(self._res)\n        self.fps = 1\n        if self._th_path is None: self._th_path = ''\n        self.log(f\"FPS: {self.fps}\")\n        self.log(f\"Theme path: {self._th_path}\")\n        self.log(self._config['ui']['display']['enabled'])\n        self.display_controller = FancyDisplay(self._config['ui']['display']['enabled'], self.fps, self._th_path, )\n        self.log('UI setup finished')\n\n    def cleanup_display(self):\n\n        if hasattr(self, 'display_controller') and self.display_controller:\n            if self.display_controller.is_running():\n                self.display_controller.stop()\n            self.display_controller = None\n            del self.display_controller\n\n    def _share_state(self, ui):\n        \"\"\"Shares a read-only copy of the internal state with the ui object.\"\"\"\n        if not hasattr(ui, 'fancy'):\n            # Create a simple namespace object on ui if it doesn't exist\n            ui.fancy = type('fancy', (object,), {})()\n        \n        # Provide a deep copy to prevent other plugins from modifying the internal state\n        ui.fancy._state = copy.deepcopy(self._state)\n        logging.debug(\"[Fancygotchi] Shared internal state with ui.fancy._state\")\n\n\n    def button_controller(self, cmd=None, screen=1):\n        screen = int(screen)\n        logging.warning(f\"Screen {screen} controlled\")\n        logging.warning(f\"cmd: {cmd}\")\n        try:\n            # verify if the button command is valid\n            if cmd:\n                button_command = cmd\n            else:\n                return\n            # if linked to the first screen\n            if button_command:\n                cmd = button_command['action']\n                if screen == 1:\n                    logging.warning(\"Screen 1 controlled\")\n                    if cmd == 'btn_start':\n                        logging.warning(\"Button start\")\n                        self.fancy_menu.toggle()\n                        self.log('button start')\n                    # Verifying if menu exists and is enabled\n                    if hasattr(self, 'fancy_menu') and self.fancy_menu.active:\n                        logging.warning(\"Fancy menu is active\")\n                        self.log(f'button_command: {cmd}')\n                        \n                        if cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:\n                            direction = cmd.split('_')[1]\n                            self.fancy_menu.navigate(direction)\n                            self.log(direction)\n                        elif cmd == 'btn_select':\n                            cmd_action = self.fancy_menu.select()\n                            logging.warning(f\"cmd_action: {cmd_action}\")\n                            self.last_cmd = cmd_action\n                else:\n                    logging.warning(\"Screen 2 controlled\")\n        except Exception as e:\n            logging.error(f\"Error in button_controller: {e}\")\n            logging.error(traceback.format_exc())\n\n    def navigate_fancymenu(self, cmd=None):\n        try:\n            if cmd:\n                menu_command = cmd\n            else:\n                return\n            if hasattr(self, 'fancy_menu'):\n                if menu_command:\n                    cmd = menu_command['action']\n                    self.log(f'menu_command: {cmd}')\n                    if cmd == 'btn_start':\n                        self.fancy_menu.toggle()\n                        self.log('start button')\n                    elif cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:\n                        direction = cmd.split('_')[1]\n                        self.fancy_menu.navigate(direction)\n                        self.log(direction)\n                    elif cmd == 'btn_select':\n                        cmd_action = self.fancy_menu.select()\n                        logging.warning(f\"cmd_action: {cmd_action}\")\n                        self.last_cmd = cmd_action\n                        #im here\n                        self.log('select')\n        except Exception as e:\n            logging.error(f\"Error in navigate_fancymenu: {e}\")\n            logging.error(traceback.format_exc())\n\n    def on_ui_update(self, ui):\n        try:            \n            if self.dispHijack:\n                if not (hasattr(self, 'display_controller') and self.display_controller and self.display_controller.is_running()):\n                    logging.debug(\"[Fancygotchi] Starting display hijack.\")\n                    self.display_controller = FancyDisplay(self._config['ui']['display']['enabled'], self.fps, self._th_path)\n                    self.display_controller.start(self._res, self.options.get('rotation', 0), self._color_mode[1])\n                    mode, submode, config = self.display_config.get('mode', 'screen_saver'), self.display_config.get('sub_mode', 'show_logo'), self.display_config.get('config', {})\n                    self.display_controller.set_mode(mode, submode, config)\n                if hasattr(ui, '_enabled') and ui._enabled:\n                    ui._enabled = False\n            #elif not self.dispHijack:\n            elif not self.dispHijack and self._config['ui']['display']['enabled']:\n                if hasattr(self, 'display_controller') and self.display_controller and self.display_controller.is_running():\n                    self.display_controller.stop()\n                #if hasattr(ui, '_enabled') and not ui._enabled and self._config['ui']['display']['enabled'] and not ui.is_rebooting():\n                if hasattr(ui, '_enabled') and not ui._enabled:\n                    ui._enabled = True\n                    ui.init_display()\n\n\n            # Check for theme updates\n            if (hasattr(ui, '_update') and ui._update.get('update')) or self.refresh:\n                is_partial = hasattr(ui, '_update') and ui._update.get('partial', False)\n                self.log(f\"Theme update triggered. Partial: {is_partial}, Refresh: {self.refresh}\")\n                \n                 # Always process the update, regardless of the theme.\n                self.theme_update(ui)\n                \n                # Crucially, always reset the flags after processing to prevent loops.\n                if hasattr(ui, '_update'):\n                    ui._update['update'] = False\n                    #ui._update['partial'] = False\n                    ui._update['partial'] = False # Reset partial flag\n                    ui._update['dict_part'] = {}\n                    self.log(\"UI update flags reset.\")\n                self.refresh = False\n               \n\n            self._res = [ui._width, ui._height]\n            self.second_screen = Image.new('RGBA', self._res, 'black')\n            \n            \n            th = self._theme['theme']\n            self._share_state(ui)\n            th_opt = th['options']\n            th_widget = th['widget']\n            rot = self.options['rotation']\n            self.pwncanvas_creation(self._res)\n            self.remove_widgets(ui)\n            ui_state = list(ui._state.items())\n            for key, state in ui_state:\n                widget_type = type(state).__name__\n                if widget_type in self.bitmap_widget:\n                    widget_type = 'Bitmap'\n                self.add_widget(ui, key, widget_type, th_widget)\n                if  widget_type == 'Text' or widget_type == 'LabeledValue':\n                    if not 'value' in self._state[key]:\n                        self._state[key].update({'value': None})\n                    self._state[key]['value'] = ui._state.get(key)\n                    \n                    if key == 'name':\n                        custom_char = th_opt[\"cursor\"]\n\n                        name_value = ui._state.get(key)\n\n                        for char in self.cursor_list:\n                            if name_value.endswith(char):\n                                name_value = name_value.rstrip(char) + f' {custom_char}'\n                                break \n\n                        self._state[key]['value'] = name_value\n\n                    if key == 'friend_name' and ui._state.get(key) != None:\n                        friend_name = ui._state.get(key)\n                        friend_name = friend_name.replace('▌', th_opt['friend_bars']).replace('│', th_opt['friend_no_bars'])\n                    \n                    value = self._state[key]['value']\n                if widget_type == 'Bitmap':\n                    \n                    if key in th_widget and th_widget[key].get('icon'):\n                        img_ref = ui._state.get_attr(key, 'image') \n                        if key in self._state and 'image_dict' in self._state[key]:\n                            img_map = self._state[key].get('image_dict')\n                            matched_custom_image = None\n                            for id_number, (img_a, img_b) in img_map.items():\n                                try:\n                                    if ImageChops.difference(img_a, img_ref).getsize() is None:\n                                        matched_custom_image = img_b\n                                        break \n                                except:\n                                    if ImageChops.difference(img_a, img_ref).getbbox() is None:\n                                        matched_custom_image = img_b\n                                        break  \n                            if matched_custom_image:\n                                self._state[key].update({'image': matched_custom_image})\n                            else:\n                                self.log('No matching image found.')\n                    else:\n                        if 'image_dict' not in self._state[key]:\n                            self._state[key]['image_dict'] = {}\n\n                        image_dict = self._state[key]['image_dict']\n                        original_img = ui._state.get_attr(key, 'image')\n\n                        corresponding_adj_img = None\n\n                        for i, (orig, adj) in image_dict.items():\n                            try:\n                                if orig == original_img:\n                                    corresponding_adj_img = adj\n                                    break\n                            except AttributeError:\n                                continue\n\n                        if corresponding_adj_img is None:\n                            i = len(image_dict)\n                            try:\n                                corresponding_adj_img = adjust_image(original_img, self._state[key]['zoom'], False, self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'])\n                                image_dict[i] = [original_img, corresponding_adj_img]\n                            except AttributeError:\n                                corresponding_adj_img = original_img\n\n                        self._state[key].update({'image_dict': image_dict})\n\n                        self._state[key].update({'image': corresponding_adj_img})\n\n            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'refresh' in self._theme['theme']['dev']:\n                self.refresh_trigger = self._theme['theme']['dev']['refresh']\n\n            if hasattr(self, 'fancy_menu'):\n                menu_command = self.last_cmd\n                if menu_command:\n                    cmd = menu_command['action']\n                    if self.dispHijack:\n                        if cmd == 'btn_start':\n                            self.dispHijack = False\n                        elif self.display_config['mode'] == 'screen_saver':\n                            if cmd == 'btn_up':\n                                self.log('switch screen saver mode')\n                                self.process_actions({'action': 'next_screen_saver'})\n                            elif cmd == 'btn_down':\n                                self.log('switch screen saver mode')\n                                self.process_actions({'action': 'previous_screen_saver'})\n                            else:\n                                self.process_actions(menu_command)\n                        elif self.display_config['mode'] == 'auxiliary':\n                            self.log('enable auxiliary mode')\n                            self.process_actions(menu_command)\n                        elif self.display_config['mode'] == 'terminal':\n                            self.process_actions(menu_command)\n                        else:\n                            self.process_actions(menu_command)\n                        \n                    elif self.fancy_menu.active:\n                        self.log(f'menu_command: {menu_command}')\n                        self.log(f'menu_command: {cmd}')\n                        if cmd == 'btn_start':\n                            self.process_actions(menu_command)\n                        elif cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:\n                            direction = cmd.split('_')[1]\n                            self.fancy_menu.navigate(direction)\n                            self.log(direction)\n                        elif cmd == 'btn_select':\n                            menu_cmd = self.fancy_menu.select()\n                            self.log(f'menu command:{menu_cmd}')\n                            try:\n                                self.process_actions(menu_cmd)\n                            except OSError as e:\n                                logging.error(f'error while processing command: {e}')\n                        else:\n                            self.process_actions(menu_command)\n                    else:\n                        self.process_actions(menu_command)\n                self.last_cmd = None\n\n            if self._i == self.refresh_trigger:\n                self.theme_update(ui)\n\n            self.drawer()\n\n            if rot == 90 or rot == 270:\n                self._pwncanvas = self._pwncanvas.rotate(90, expand=True)\n\n            if hasattr(ui, '_pwncanvas_tmp') and ui._pwncanvas_tmp == None:\n                setattr(ui, '_pwncanvas_tmp', self._pwncanvas)\n            if hasattr(ui, '_pwncanvas') and ui._pwncanvas == None:\n                setattr(ui, '_pwncanvas', self._pwncanvas)\n\n            if self._imax != None:\n                if self._imax - 1 == self._i:\n                    self._i = 0\n                else:\n                    self._i += 1\n\n        except Exception as e:\n            self.log(\"non fatal error while updating Fancygotchi: %s\" % e)\n            self.log(traceback.format_exc())\n    \n    # Theme section\n    def generate_default_config(self, config_path, actual_state):\n        default_config = {\n            'theme': {\n                'options': copy.deepcopy(self._default['theme']['options']),\n                'menu': {\n                    'options': copy.deepcopy(self._default_menu),\n                },\n                'widget': {}\n            }\n        }\n\n        for widget_name, state in actual_state.items():\n            widget_type = state['widget_type']\n            if widget_type in self.bitmap_widget:\n                widget_type = 'Bitmap'\n            default_widget_config = self.widget_defaults.get(widget_type, {})\n            \n            default_config['theme']['widget'][widget_name] = copy.deepcopy(default_widget_config)\n\n        for widget_name, state in self._state_default.items():\n            if widget_name in default_config['theme']['widget']:\n                default_config['theme']['widget'][widget_name].update(state)\n\n                if 'widget_type' in default_config['theme']['widget'][widget_name]:\n                    del default_config['theme']['widget'][widget_name]['widget_type']\n\n        with open(config_path, 'w') as f:\n            toml.dump(default_config, f)\n\n        logging.debug(f\"Default configuration saved to {config_path}\")\n        return default_config\n\n    def refresh_plugins(self):\n        new_plugs = ''\n        if 'custom_plugins' in self._agent._config['main']:\n            path = self._agent._config['main']['custom_plugins']\n            logging.debug(\"loading plugins from %s\" % (path))\n            for filename in glob.glob(os.path.join(path, \"*.py\")):\n                plugin_name = os.path.basename(filename.replace(\".py\", \"\"))\n                if not plugin_name in plugins.database:\n                    logging.debug(\"New plugin: %s\" % (plugin_name))\n                    plugins.database[plugin_name] = filename\n                    new_plugs += \",%s\" % plugin_name\n            if new_plugs != '':\n                self.log(\"found new:%s\" % (new_plugs))\n\n    def load_and_run_module(self, module_path):\n        if module_path.startswith('/'): module_file_path = module_path\n        else: module_file_path = os.path.join(self._th_path, 'scripts', module_path)\n        \n        if not os.path.exists(module_file_path):\n            self.log(f\"Module file {module_file_path} does not exist.\")\n            return\n        \n        try:\n            module_name = os.path.splitext(os.path.basename(module_file_path))[0] \n            spec = importlib.util.spec_from_file_location(module_name, module_file_path)\n            module = importlib.util.module_from_spec(spec)\n            spec.loader.exec_module(module)\n            \n            if hasattr(module, 'main'): \n                module.main()\n            else:\n                self.log(f\"Module {module_name} imported successfully, but no 'main' function found.\")\n        \n        except Exception as e:\n            logging.error(f\"Error while loading and executing module {module_file_path}: {e}\")\n            logging.error(traceback.format_exc())\n\n    def process_actions(self, command):\n        if command is None:\n            logging.error(\"[Fancygotchi] Action is None, unable to process.\")\n            return\n        try:\n            action = command.get('action')\n            mode = command.get('mode', 'manu')\n            self.actions_log.append(action)\n            self.actions_log = self.actions_log[-12:]\n            self.log(f'Action: {action}')\n\n            if action == 'submenu':\n                self.fancy_menu.navigate(\"right\")\n            elif action == 'btn_start':\n                self.fancy_menu.toggle()\n            elif action == 'plugin':\n                # http://10.0.0.2:8080/plugins/Fancygotchi/plugin?name=bt-tether&enable=False\n                name = command.get('name')\n                state = command.get('enable')\n\n                if name and name != 'None':  # Validate the plugin name\n                    self.log(f'Plugin command received: {name}, state: {state}')\n\n                    # Convert state to a boolean if it's provided as a string\n                    enable_state = state.lower() == 'true' if isinstance(state, str) else bool(state)\n\n                    # Attempt to toggle the plugin\n                    try:\n                        is_change = toggle_plugin(name, enable=enable_state)\n                        self.log(f\"Plugin '{name}' {'changed state' if is_change else 'did not change state'} to {'enabled' if enable_state else 'disabled'}.\")\n                    except Exception as e:\n                        self.log(f\"Error toggling plugin '{name}': {e}\")\n                else:\n                    self.log(\"Invalid plugin name provided for menu_plugin action.\")\n\n            elif action == 'refresh_plugins':\n                self.refresh_plugins()\n            elif action == 'shutdown':\n                pwnagotchi.shutdown()\n            elif action == 'restart':\n                pwnagotchi.restart(mode)\n            elif action == 'reboot':\n                pwnagotchi.reboot(mode)\n            elif action == 'theme_select':\n                name = command.get('name')\n                rotation = command.get('rotation')\n                self.theme_save_config(name, rotation)\n                self.refresh = True\n            elif action == 'theme_refresh':\n                self.refresh = True\n            elif action == 'stealth_mode':\n                self.stealth_mode = not self.stealth_mode\n                self.refresh = True\n            elif action == 'switch_screen_mode':\n                try:\n                    self.display_config['mode'] = self.display_controller.switch_mode()\n                except:\n                    self.display_config['mode'] = self.screen_modes[(self.screen_modes.index(self.display_config['mode']) + 1) % len(self.screen_modes)]\n            elif action == 'switch_screen_mode_reverse':\n                try:\n                    self.display_config['mode'] = self.display_controller.switch_mode('previous')\n                except:\n                    self.display_config['mode'] = self.screen_modes[(self.screen_modes.index(self.display_config['mode']) - 1) % len(self.screen_modes)]\n            elif action == 'enable_second_screen':\n                self.dispHijack = True\n                self.fancy_menu.active = False\n            elif action == 'disable_second_screen':\n                self.log('disable second screen')\n                self.dispHijack = False\n            elif action == 'next_screen_saver':\n                self.log('next screen saver')\n                try:\n                    self.display_config['sub_mode'] = self.display_controller.switch_screen_saver_submode('next')\n                except:\n                    self.display_config['sub_mode'] = self.screen_saver_modes[(self.screen_saver_modes.index(self.display_config['sub_mode']) + 1) % len(self.screen_saver_modes)]\n            elif action == 'previous_screen_saver':\n                self.log('previous screen saver')\n                try:\n                    self.display_config['sub_mode'] =  self.display_controller.switch_screen_saver_submode('previous')\n                except:\n                    self.display_config['sub_mode'] = self.screen_saver_modes[(self.screen_saver_modes.index(self.display_config['sub_mode']) + 1) % len(self.screen_saver_modes)]\n            elif action == 'run_bash':\n                script = command.get('file')\n                if script.startswith('/'): script_path = script\n                else: script_path = os.path.join(self._th_path, 'scripts', script)\n                if os.path.exists(script_path):\n                    self.log(f'Running script: {script_path}')\n                    os.system(f'chmod +x {script_path}')\n                    self.log(f'Running command: {script_path}')\n                    exit_code = os.system(f'{script_path}')\n                    self.log(f\"Script exited with code: {exit_code}\")\n                else:\n                    self.log(f\"Script not found: {script_path}\")\n            elif action == 'run_python':\n                file_path = command.get('file')\n                self.load_and_run_module(file_path)\n\n        except Exception as e:\n            logging.error(f'error while processing menu command: {e}')\n\n    def theme_creator(self, theme_name, state, oriented=False, resolution=False):\n        themes_folder = os.path.join(self._plug_root, 'themes')\n        res = ''\n\n        new_theme_folder = os.path.join(themes_folder, theme_name)\n        \n        if os.path.exists(new_theme_folder):\n            self.log(f\"Theme '{theme_name}' already exists. Skipping creation.\")\n            return False\n\n        os.makedirs(new_theme_folder, exist_ok=True)\n\n        folders = ['config', 'img', 'fonts']\n        for folder in folders:\n            os.makedirs(os.path.join(new_theme_folder, folder), exist_ok=True)\n\n        img_subfolders = ['bg', 'face', 'friend_face', 'widgets', 'icons']\n        for subfolder in img_subfolders:\n            os.makedirs(os.path.join(new_theme_folder, 'img', subfolder), exist_ok=True)\n\n        if resolution:\n            res = f'{self._res[0]}x{self._res[1]}'\n            os.makedirs(os.path.join(new_theme_folder, 'config', res), exist_ok=True)\n\n        info_json = {\n            \"author\": \"\",\n            \"version\": \"1.0.0\",\n            \"resolutions\": \"\",\n            \"display\": \"\",\n            \"plugins\": [\"\", \"\"],\n            \"notes\": \"\"\n        }\n        with open(os.path.join(new_theme_folder, 'info.json'), 'w') as f:\n            json.dump(info_json, f, indent=2)\n\n        original_css_backup = os.path.join(self._pwny_root, 'ui/web/static/css/style.css.backup')\n        original_css_path = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')\n        new_css_path = os.path.join(new_theme_folder, 'style.css')\n\n        with open(new_css_path, 'w+') as f:\n            f.write(CSS)\n\n        config_path = os.path.join(new_theme_folder, 'config')\n        if resolution:\n            config_path_res = os.path.join(config_path, res)\n            if oriented:\n                config_path = os.path.join(config_path_res, 'config-h.toml')\n                self.generate_default_config(config_path, state)\n                config_path = os.path.join(config_path_res, 'config-v.toml')\n                self.generate_default_config(config_path, state)\n            else:\n                config_path = os.path.join(config_path_res, 'config.toml')\n                self.generate_default_config(config_path, state)\n        else:\n            if oriented:\n                config_path_uni = config_path\n                config_path = os.path.join(config_path_uni, 'config-h.toml')\n                self.generate_default_config(config_path, state)\n                config_path = os.path.join(config_path_uni, 'config-v.toml')\n                self.generate_default_config(config_path, state)\n            else:\n                config_path = os.path.join(config_path, 'config.toml')\n                self.generate_default_config(config_path, state)\n\n        return True\n\n    def theme_selector(self, config, boot=False):\n        self._theme = {} \n        th_path = None\n        self._theme_name = 'Default'\n        try:\n            if not boot: self.log('Theme selector')\n            fancy_opt = config['main']['plugins']['Fancygotchi']\n            self.options['rotation'] = fancy_opt.get('rotation', 0)\n\n            self._theme = copy.deepcopy(self._default)\n            size = f'{self._res[0]}x{self._res[1]}'\n            if 'theme' in fancy_opt and fancy_opt['theme'] != '':\n                theme = fancy_opt['theme']\n                self._theme_name = theme\n                rot = fancy_opt['rotation']\n                th_path = os.path.join(self._plug_root, 'themes', theme)\n                self._th_path = th_path\n\n                cfg_path = os.path.join(th_path, \"config\")\n\n                if not os.path.exists(cfg_path):\n                    self.log(f\"Warning: Theme config folder {cfg_path} does not exist, loading default theme.\")\n                    self._theme = copy.deepcopy(self._default)\n                    return\n\n                toml_files = [f for f in os.listdir(cfg_path) if f.endswith('.toml')]\n                \n                if len(toml_files) == 1:\n                    cfg_file = toml_files[0]\n                elif 'config-v.toml' in toml_files and 'config-h.toml' in toml_files:\n                    cfg_file = 'config-v.toml' if rot in [90, 270] else 'config-h.toml'\n                else:\n                    size_folder = os.path.join(cfg_path, size)\n                    if os.path.exists(size_folder):\n                        size_toml_files = [f for f in os.listdir(size_folder) if f.endswith('.toml')]\n                        if len(size_toml_files) == 1:\n                            cfg_file = os.path.join(size, size_toml_files[0])\n                        else:\n                            cfg_file = os.path.join(size, 'config-v.toml' if rot in [90, 270] else 'config-h.toml')\n                    else:\n                        cfg_file = 'config-h.toml'\n\n                self.cfg_path = os.path.join(cfg_path, cfg_file)\n\n                if os.path.exists(self.cfg_path):\n                    with open(self.cfg_path, 'r') as f:\n                        self._theme = toml.load(f)\n                else:\n                    self._theme = copy.deepcopy(self._default)\n\n            if th_path:\n                css_src = os.path.join(th_path, 'style.css')\n                css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')\n                css_backup = css_dst + '.backup'\n                if os.path.exists(css_src):\n                    if not os.path.exists(css_backup):\n                        copyfile(css_dst, css_backup)\n                    copyfile(css_src, css_dst)\n\n                img_src = os.path.join(th_path, 'img')\n                img_dst = os.path.join(self._pwny_root, 'ui/web/static')\n                icon_src = os.path.join(th_path, 'img', 'icons', 'favicon.png')\n                icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')\n                icon_bkup = icon_dst + '.backup'\n                icon_dst_dir = os.path.dirname(icon_dst)\n                if not os.path.exists(icon_dst_dir):\n                    os.makedirs(icon_dst_dir)\n\n                if os.path.exists(icon_src):\n                    if not os.path.exists(icon_bkup):\n                        if not os.path.exists(icon_dst):\n                            copyfile(icon_src, icon_dst)\n                        else:\n                            copyfile(icon_dst, icon_bkup)\n                    copyfile(icon_src, icon_dst)\n                else:\n                    if os.path.exists(icon_bkup):\n                        copyfile(icon_bkup, icon_dst)\n                        os.remove(icon_bkup)\n                if os.path.exists(img_dst):\n                    os.system('rm %s/img' % (img_dst))\n                if os.path.exists(img_src):\n                    os.system('ln -s %s %s' % (img_src, img_dst))\n            else:\n                icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')\n                icon_bkup = icon_dst + '.backup'\n                css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')\n                css_backup = css_dst + '.backup'\n                if os.path.exists(css_backup):\n                    copyfile(css_backup, css_dst)\n                    os.remove(css_backup)\n                if os.path.exists(icon_bkup):\n                    copyfile(icon_bkup, icon_dst)\n                    os.remove(icon_bkup)\n\n            if self._theme['theme']['options'].get('faces'):\n                self._config['ui']['faces'] = self._theme['theme']['options']['faces']\n            faces.load_from_config(self._config['ui']['faces'])\n\n            if not boot:self.log(f'Theme: {self._theme_name}')\n\n        except Exception as e:\n            self.log(f\"Error in theme selector: {str(e)}\")\n            self.log(traceback.format_exc())\n            return None\n\n    def save_screenshot(self, theme_name, screenshot_url, headers):\n        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')\n        theme_folder_path = os.path.join(screenshots_path, theme_name)\n        os.makedirs(theme_folder_path, exist_ok=True)\n        response = requests.get(screenshot_url, headers=headers).content\n        screenshot_path = os.path.join(theme_folder_path, 'screenshot.png')\n        with open(screenshot_path, 'wb') as f:\n            f.write(response)\n        return os.path.join('repo_screenshots', theme_name, 'screenshot.png')\n\n    def fetch_themes(self):\n        themes = {}\n        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')\n        try:\n            if os.path.exists(screenshots_path):\n                shutil.rmtree(screenshots_path)\n            headers = {\"Authorization\": f\"Bearer {self.gittoken}\"} if self.gittoken else {}\n            response = requests.get(THEMES_REPO, headers=headers)\n            response.raise_for_status()\n            for item in response.json():\n                if item[\"type\"] == \"dir\":\n                    theme_name = item[\"name\"]\n                    self.log(f\"Fetching theme: {theme_name}\")\n                    theme_url = item[\"url\"]\n                    themes[theme_name] = {\"info\": None, \"screenshot\": None}\n                    theme_contents = requests.get(theme_url, headers=headers).json()\n                    for file in theme_contents:\n                        if file[\"name\"] == \"info.json\":\n                            info_url = file[\"download_url\"]\n                            info_data = requests.get(info_url, headers=headers).json()\n                            themes[theme_name][\"info\"] = info_data\n                        elif file[\"name\"] == \"img\" and file[\"type\"] == \"dir\":\n                            img_folder_url = file[\"url\"]\n                            img_contents = requests.get(img_folder_url, headers=headers).json()\n                            if isinstance(img_contents, list):\n                                for img_file in img_contents:\n                                    if isinstance(img_file, dict) and img_file.get(\"name\") == \"screenshot.png\":\n                                        local_screenshot_path = self.save_screenshot(theme_name, img_file[\"download_url\"], headers)\n                                        themes[theme_name][\"screenshot\"] = local_screenshot_path\n            sorted_themes = dict(sorted(themes.items(), key=lambda item: item[0].lower()))\n            self.log(\"Themes fetched successfully:\")\n            for theme, info in sorted_themes.items():\n                version = info[\"info\"].get(\"version\") if info[\"info\"] else \"Unknown\"\n                self.log(f\"{theme}: Version {version}, Screenshot: {info['screenshot']}\")\n            return sorted_themes\n\n        except requests.RequestException as e:\n            logging.error(f\"Error fetching themes: {e}\")\n            return {}\n\n    def theme_downloader(self, theme_name):\n        try:\n            headers = {\"Authorization\": f\"Bearer {self.gittoken}\"} if self.gittoken else {}\n            theme_contents_url = os.path.join(THEMES_REPO, theme_name)\n            response = requests.get(theme_contents_url, headers=headers)\n            response.raise_for_status()\n            contents = response.json()\n            temp_dir = tempfile.mkdtemp()\n            temp_theme_path = os.path.join(temp_dir, theme_name)\n            final_path = os.path.join(self._plug_root, \"themes\", theme_name)\n            os.makedirs(temp_theme_path, exist_ok=True)\n            def download_content(contents, current_path):\n                for item in contents:\n                    item_path = os.path.join(current_path, item['name'])\n                    if item['type'] == 'dir':\n                        os.makedirs(item_path, exist_ok=True)\n                        dir_response = requests.get(item['url'], headers=headers)\n                        dir_response.raise_for_status()\n                        download_content(dir_response.json(), item_path)\n                    else:\n                        file_response = requests.get(item['download_url'], headers=headers)\n                        file_response.raise_for_status()\n                        with open(item_path, 'wb') as f:\n                            f.write(file_response.content)\n            download_content(contents, temp_theme_path)\n            if os.path.exists(final_path):\n                shutil.rmtree(final_path)\n            shutil.move(temp_theme_path, final_path)\n            shutil.rmtree(temp_dir)\n            self.log(f\"Theme {theme_name} downloaded successfully to {final_path}\")\n\n        except requests.RequestException as e:\n            logging.error(f\"Error downloading themes: {e}\")\n            logging.error(traceback.format_exc())\n            if 'temp_dir' in locals():\n                shutil.rmtree(temp_dir)\n\n    def save_active_config(self, data):\n        cfg_path = self.cfg_path\n        self.log(f\"Saving active config to: {self.cfg_path}\")\n            \n        if os.path.exists(cfg_path):\n            os.remove(cfg_path)\n\n        with open(cfg_path, 'w') as f:\n            toml.dump(data, f)\n\n    def theme_save_config(self, theme, rotation):\n        if self._config['ui'] ['display']['rotation'] != 0:\n            self._config['ui'] ['display']['rotation'] = 0\n        self._config['main']['plugins']['Fancygotchi']['rotation'] = int(rotation)\n        if theme == 'Default': theme = ''\n        self._config['main']['plugins']['Fancygotchi']['theme'] = theme\n        \n        \n        self.log('Theme save config')\n        pwnagotchi.config = merge_config(self._config, pwnagotchi.config)\n        if self._agent:\n            self._agent._config = merge_config(self._config, pwnagotchi.config)\n        save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml')\n    \n    def reload_voice(self, ui, lang=None):\n        \"\"\"\n        Reloads the voice module and updates the UI's voice instance\n        without restarting the pwnagotchi service.\n        Accepts an optional 'lang' parameter to specify the voice language.\n        \"\"\"\n        try:\n            self.log(f\"Reloading voice module for language: '{lang}'\")\n            \n            # If no language is specified, use the one from the main config\n            if lang is None:\n                lang = ui._config['main']['lang']\n\n            # If using fancyvoice, check for .mo and compile from .po if needed\n            if lang == 'fancyvoice':\n                localedir = os.path.join(self._pwny_root, 'locale')\n                mo_path = os.path.join(localedir, lang, 'LC_MESSAGES', 'voice.mo')\n                po_path = os.path.join(localedir, lang, 'LC_MESSAGES', 'voice.po')\n\n                if not os.path.exists(mo_path) and os.path.exists(po_path):\n                    self.log(f\".mo file not found for '{lang}'. Compiling from .po file.\")\n                    mo_data = _compile_po_to_mo(po_path)\n                    if mo_data:\n                        try:\n                            with open(mo_path, 'wb') as f:\n                                f.write(mo_data)\n                            self.log(f\"Successfully compiled {po_path} to {mo_path}\")\n                        except IOError as e:\n                            logging.error(f\"[Fancygotchi] Could not write .mo file to {mo_path}: {e}\")\n                elif not os.path.exists(po_path):\n                     logging.warning(f\"[Fancygotchi] voice.po file not found at {po_path}. Cannot compile .mo file.\")\n\n\n            # Crucial step: Clear the gettext cache to force it to find new .mo files.\n            # gettext.clearcache() was added in Python 3.8. This is a fallback for older versions.\n            if hasattr(gettext, 'clearcache'):\n                gettext.clearcache()\n            elif hasattr(gettext, '_translations'):\n                gettext._translations.clear()\n            \n            logging.debug(\"[Fancygotchi] gettext cache cleared.\")\n\n\n            # Find the voice module in sys.modules\n            if 'pwnagotchi.voice' in sys.modules:\n                # Reload the module to pick up any changes\n                voice_module = importlib.reload(sys.modules['pwnagotchi.voice'])\n                # Create a new instance of the reloaded Voice class\n                ui._voice = voice_module.Voice(lang=lang)\n                self.log(f\"Voice module reloaded successfully for language: '{lang}'\")\n        except Exception as e:\n            logging.error(f\"[Fancygotchi] Error reloading voice: {e}\")\n\n    def setup_menu(self, th_menu):\n        if hasattr(self, 'fancy_menu'):\n            del self.fancy_menu\n        menu_theme = copy.deepcopy(self._default_menu)\n        menu_opt = th_menu.get('options', {})\n        menu_theme.update(menu_opt)\n        custom_menus = {}\n        if 'menu' in self._theme.get('theme', {}):\n            custom_menus = copy.deepcopy(self._theme.get(\"theme\", {}).get(\"menu\", {}))\n            custom_menus.pop('options', None)\n        diagnostic_path = \"/usr/local/bin/diagnostic.sh\"\n        if os.path.exists(diagnostic_path):\n            os.remove(diagnostic_path)\n        with open(diagnostic_path, \"w\") as diagnostic_file:\n            diagnostic_file.write(DIAGNOSTIC)\n        os.system(f'chmod +x {diagnostic_path}')\n        self.fancy_menu = FancyMenu(self, menu_theme, custom_menus)\n        try:\n            fancytools_content = FANCYTOOLS.replace(\"{pyenv}\", self.pyenv)\n            fancytools_path = \"/usr/local/bin/fancytools\"\n            logging.debug(f\"Writing content to {fancytools_path}\")\n            if os.path.exists(fancytools_path):\n                os.remove(fancytools_path)\n\n            with open(fancytools_path, \"w\") as fancytools_file:\n                fancytools_file.write(fancytools_content)\n\n            os.system(f'chmod +x {fancytools_path}')\n            self.running = True\n        except Exception as e:\n            logging.error(f\"An unexpected error occurred: {e}\")\n\n    def theme_update(self, ui, boot=False):\n        if not self.ready:\n            return\n        th_opt = copy.deepcopy(self._default['theme']['options'])\n        if not boot:\n            self.log('Theme update')\n        if (hasattr(ui, '_update') and ui._update.get('update')) or self.refresh:\n            if not (hasattr(ui, '_update') and ui._update.get('partial', False)):\n                self._state = {}\n\n                with open('/etc/pwnagotchi/config.toml', 'r') as f:\n                    f_toml = toml.load(f)\n                    try:\n                        self.options['rotation'] = f_toml['main']['plugins']['Fancygotchi']['rotation']\n                    except:\n                        self.options['rotation'] = 0\n                        f_toml['main']['plugins']['Fancygotchi']['rotation'] = self.options['rotation']\n\n                    try:\n                        self.options['theme'] = f_toml['main']['plugins']['Fancygotchi']['theme']\n                    except:\n                        self.options['theme'] = ''\n                        f_toml['main']['plugins']['Fancygotchi']['theme'] = self.options['theme']\n\n                rot = self.options['rotation']\n                if self.options['theme'] == '':\n                    th_name = 'Default'\n                else:\n                    th_name = self.options['theme']\n                pwnagotchi.config['main']['plugins']['Fancygotchi']['rotation'] = rot\n                pwnagotchi.config['main']['plugins']['Fancygotchi']['theme'] = th_name\n                pwnagotchi.config = merge_config(f_toml, pwnagotchi.config)\n                if self._agent:\n                    self._agent._config = merge_config(f_toml, pwnagotchi.config)\n                self.log(f'theme name: {th_name}, rotation: {rot}')\n                save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml')\n                self.theme_selector(f_toml, boot)\n    \n                th = self._theme['theme']\n                th_opt = th['options']\n                th_widget = th['widget']\n                th_menu = th.get('menu', {})\n\n                # Start of Fancygotchi voice reload modification\n                self.log(\"Checking for custom voice in theme...\")\n                locale_path = os.path.join(self._pwny_root, 'locale', 'fancyvoice')\n                self.log(f\"Locale path: {locale_path}\")\n                self.log(self._th_path)\n                self.log(f\"Target locale path: {locale_path}\")\n\n                # Check if the theme has a custom voice\n                if self._th_path and os.path.isdir(os.path.join(self._th_path, 'voice')) and th_name != 'Default':\n                    theme_voice_path = os.path.join(self._th_path, 'voice')\n                    self.log(\"Custom voice found. Applying 'fancyvoice'.\")\n                    if os.path.islink(locale_path) or os.path.exists(locale_path):\n                        os.unlink(locale_path)\n                        logging.debug(\"[Fancygotchi] Removed existing fancyvoice symlink.\")\n                    os.symlink(os.path.abspath(theme_voice_path), locale_path)\n                    self.reload_voice(ui, lang='fancyvoice')\n                else:\n                    self.log(\"No custom voice in theme. Reverting to system default voice.\")\n                    if os.path.islink(locale_path):\n                        os.unlink(locale_path)\n                    self.reload_voice(ui, lang=ui._config['main']['lang'])\n                \n                self.setup_menu(th_menu)\n            else:\n                self.log('Partial update received.')                \n                th = self._theme['theme']\n                menu_updated = False\n    \n                rot = self.options['rotation']\n                if 'options' in ui._update['dict_part']:\n                    self.log(\"Partial update: Applying options changes.\")\n                    th_options = ui._update['dict_part']['options']\n                    th['options'].update(th_options)\n                if 'widget' in ui._update['dict_part']:\n                    self.log(\"Partial update: Applying widget changes.\")\n                    th_widget = ui._update['dict_part']['widget']\n                    for widget_name, widget_data in th_widget.items():\n                        if widget_name in th['widget']:\n                            th['widget'][widget_name].update(widget_data)\n                        else:\n                            th['widget'][widget_name] = widget_data\n                if 'menu' in ui._update['dict_part']:\n                    self.log(\"Partial update: Applying menu changes.\")\n                    menu_updated = True\n                    th_menu_update = ui._update['dict_part']['menu']\n                    for menu_key, menu_data in th_menu_update.items():\n                        if menu_key in th.get('menu', {}):\n                            th['menu'][menu_key].update(menu_data)\n                        else:\n                            th.setdefault('menu', {})[menu_key] = menu_data\n                \n                if menu_updated:\n                    th_menu = th.get('menu', {})\n                    self.setup_menu(th_menu)\n\n                th_opt = th['options']\n                \n            if th_opt:\n                if 'font' in th_opt and th_opt['font'] != '':\n                    ft = th_opt['font_sizes']\n                    self.font_name  = th_opt['font']\n                    self.setup_font(ft[0], ft[1], ft[2], ft[3], ft[4], ft[5])\n                if 'font_awesome' in th_opt and th_opt['font_awesome'] != '':\n                    self.f_awesome_name = th_opt['font_awesome']\n                if 'color_mode' in th_opt and th_opt['color_mode'] != '':\n                    self._color_mode = th_opt['color_mode']\n                    setattr(ui, '_web_mode', self._color_mode[0])\n                    setattr(ui, '_hw_mode', self._color_mode[1])\n                if hasattr(th_opt, 'main_text_color') and th_opt.get('main_text_color', []) != []:\n                    self._icolor = 0\n                if hasattr(th_opt, 'base_text_color') and th_opt.get('base_text_color', []) != []:\n                    self._icolor = 0\n                self.fps = th_opt.get('second_screen_fps', 1)\n                self.webui_fps = int(1000*th_opt.get('webui_fps', 1))\n                if rot in (90, 270):\n                    startname = f'{self._res[1]}x{self._res[0]}' \n                    w = self._res[1]\n                    h = self._res[0]\n                elif rot in (0, 180):\n                    startname = f'{self._res[0]}x{self._res[1]}' \n                    w = self._res[0]\n                    h = self._res[1]\n                if  th_opt.get('bg_fg_select', 'manu') not in ('auto', 'manu'):\n                    th_opt['bg_fg_select'] = 'manu'\n                screen_mode = th_opt.get('screen_mode', 'screen_saver')\n                if screen_mode in self.screen_modes:\n                    self.display_config['mode'] = screen_mode\n                screen_saver = th_opt.get('screen_saver', 'show_logo')\n                if screen_saver in self.screen_saver_modes:\n                    self.display_config['sub_mode'] = screen_saver\n                self.display_config['second_screen_webui'] = th_opt.get('second_screen_webui', False)\n                bgfg_mode = th_opt.get('bg_fg_select', 'manu')\n                if self._th_path is not None:\n                    bg_folder_path = os.path.join(self._th_path, 'img', 'bg')\n                    if bgfg_mode == 'manu':\n                        bga_name = th_opt.get('bg_anim_image', '')\n                        bga_path = os.path.join(bg_folder_path, bga_name)\n                        bg_name = th_opt.get('bg_image', '')\n                        bg_path = os.path.join(bg_folder_path, bg_name)\n                        fg_name = th_opt.get('fg_image', '')\n                        fg_path = os.path.join(bg_folder_path, fg_name)\n                    elif bgfg_mode == 'auto':\n                        valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp')\n                        valid_anim_extensions = ('.gif')\n\n                        bga_fname = f'{startname}bga'\n                        bg_fname = f'{startname}bg'\n                        fg_fname = f'{startname}fg'\n\n                        bga_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                        if f.lower().startswith(bga_fname) and f.lower().endswith(valid_anim_extensions)), None)\n                        \n                        bg_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                        if f.lower().startswith(bg_fname) and f.lower().endswith(valid_extensions)), None)\n                        \n                        fg_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                        if f.lower().startswith(fg_fname) and f.lower().endswith(valid_extensions)), None)\n\n                        if not bga_path:\n                            bga_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                            if f.lower().startswith('bga') and f.lower().endswith(valid_anim_extensions)), None)\n\n                        if not bg_path:\n                            bg_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                            if f.lower().startswith('bg') and f.lower().endswith(valid_extensions)), None)\n\n                        if not fg_path:\n                            fg_path = next((os.path.join(bg_folder_path, f) for f in os.listdir(bg_folder_path) \n                                            if f.lower().startswith('fg') and f.lower().endswith(valid_extensions)), None)\n\n                    self._frames = []\n                    self._i = 0\n                    if ('bg_anim_image' in th_opt and th_opt['bg_anim_image'] != '') or bga_path is not None:\n                        if bga_path and os.path.exists(bga_path) and not os.path.isdir(bga_path):\n                            gif = Image.open(bga_path)\n                            self._i = 0\n                            self._frames = []\n                            frames = ImageSequence.Iterator(gif)\n                            for frame in frames:\n                                canvas = Image.new('RGBA', (w, h), (0, 0, 0, 0))\n                                frame = frame.convert(\"RGBA\")\n                                frame = image_mode(canvas, frame, th_opt.get('bg_mode', 'normal'))\n                                self._frames.append(frame)\n                            self._imax = len(self._frames)\n\n                    if ('bg_image' in th_opt and th_opt['bg_image'] != '') or bg_path is not None:\n                        canvas = Image.new('RGBA', (w, h), (0, 0, 0, 0))\n\n                        if bg_path and os.path.exists(bg_path) and not os.path.isdir(bg_path):\n                            bg_tmp = Image.open(bg_path)\n                            bg_tmp = bg_tmp.convert(\"RGBA\")\n                            self._bg = image_mode(canvas, bg_tmp, th_opt.get('bg_mode', 'normal'))\n                        else:\n                            self._bg = ''\n                    else:\n                        self._bg = ''\n\n                    if ('fg_image' in th_opt and th_opt['fg_image'] != '') or fg_path is not None:\n                        canvas = Image.new('RGBA', (w, h), (0, 0, 0, 0))\n                        if fg_path and os.path.exists(fg_path) and not os.path.isdir(fg_path):\n                            fg_tmp = Image.open(fg_path) \n                            fg_tmp = fg_tmp.convert(\"RGBA\")\n                            self._fg = image_mode(canvas, fg_tmp, th_opt.get('fg_mode', 'normal'))\n                        else:\n                            self._fg = ''\n                    else:\n                        self._fg = ''\n\n                    if 'boot_max_loops' in th_opt and th_opt['boot_max_loops'] != 0:\n                        th_opt['boot_max_loops'] = int(th_opt['boot_max_loops'])\n                    else:\n                        th_opt['boot_max_loops'] = 1\n                    if 'boot_total_duration' in th_opt and th_opt['boot_total_duration'] != 0:\n                        th_opt['boot_total_duration'] = int(th_opt['boot_total_duration'])\n                    else:\n                        th_opt['boot_total_duration'] = 5\n                    boot_anim_file = '/usr/local/bin/boot_animation.py'\n                    if 'boot_animation' in th_opt and th_opt['boot_animation'] and self._config['ui']['display']['enabled']:\n                        img_path = os.path.join(self._th_path, 'img', 'boot')\n                        color_mode_web, color_mode_hw = th_opt['color_mode']\n                        boot_anim_py = BOOT_ANIM.format(img_path=img_path, width=self._res[0], height=self._res[1], max_loops=th_opt['boot_max_loops'], total_duration=th_opt['boot_total_duration'], rotation=rot, color_mode=color_mode_hw)\n                        with open(boot_anim_file, 'w') as f:\n                            f.write(boot_anim_py)\n                    else:\n                        if os.path.exists(boot_anim_file):\n                            os.remove(boot_anim_file)\n\n    def theme_list(self):\n        themes_path = os.path.join(self._plug_root, 'themes')\n        themes = []\n        if not os.path.exists(themes_path):\n            os.makedirs(themes_path)\n        items = os.listdir(themes_path)\n        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/screenshots')\n        if os.path.exists(screenshots_path):\n            os.system(f'rm -r {screenshots_path}')\n        if not os.path.exists(screenshots_path):\n            os.makedirs(screenshots_path, exist_ok=True)\n            image = Image.new('RGBA', self._res, (0, 0, 0, 0))\n            image_path = os.path.join(screenshots_path, 'screenshot.png')\n            image.save(image_path)\n        for item in items:\n            if os.path.isdir(os.path.join(themes_path, item)):\n                themes.append(item)\n                screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/screenshots')\n                screenshot_path = os.path.join(themes_path, item, 'img', 'screenshot.png')\n                subfolder = os.path.join(screenshots_path, item)\n                \n                if os.path.exists(screenshot_path):\n                    if not os.path.exists(subfolder):\n                        os.makedirs(subfolder, exist_ok=True)\n                    os.system(f'cp {screenshot_path} {subfolder}')\n\n        return sorted(themes, key=lambda x: x.lower())\n\n    def change_font(self, old_font, new_font=None, size_offset=None):\n        if new_font == None:\n            new_font = self._theme['theme']['options']['status_font']\n        if size_offset == None:\n            size_offset = self._theme['theme']['options']['size_offset']\n        return ImageFont.truetype(self.get_font_path(new_font), size=old_font.size + size_offset)\n\n    def theme_export(self, theme_name):\n        self.log(f\"Exporting theme {theme_name}\")\n        try:\n            themes_folder = os.path.join(self._plug_root, 'themes')\n            theme_path = os.path.join(themes_folder, theme_name)\n            if not os.path.exists(theme_path):\n                return make_response(jsonify({\"error\": \"Theme not found\"}), 404)\n\n            zip_filename = f\"{theme_name}_export.zip\"\n            zip_path = os.path.join(tempfile.gettempdir(), zip_filename)\n\n            with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n                for root, _, files in os.walk(theme_path):\n                    for file in files:\n                        file_path = os.path.join(root, file)\n                        arcname = os.path.join(theme_name, os.path.relpath(file_path, theme_path))\n                        zipf.write(file_path, arcname)\n\n            return send_file(zip_path, as_attachment=True, download_name=zip_filename)\n        except Exception as e:\n            logging.error(f\"Error exporting theme {theme_name}: {str(e)}\")\n            return f\"Error exporting theme: {str(e)}\", 500\n\n    def get_font_path(self, font_name):\n        if '.' not in font_name:\n            return font_name\n        else:\n            return os.path.join(self._th_path, 'fonts', font_name)\n\n    def setup_font(self, bold, bold_small, medium, huge, bold_big, small):\n        self.Small = ImageFont.truetype(self.get_font_path(self.font_name), small)\n        self.Medium = ImageFont.truetype(self.get_font_path(self.font_name), medium)\n        self.BoldSmall = ImageFont.truetype(self.get_font_path(self.font_bold_name), bold_small)\n        self.Bold = ImageFont.truetype(self.get_font_path(self.font_bold_name), bold)\n        self.BoldBig = ImageFont.truetype(self.get_font_path(self.font_bold_name), bold_big)\n        self.Huge = ImageFont.truetype(self.get_font_path(self.font_bold_name), huge)\n\n    def rgba_text(self, text, tfont, color='black', width=None, height=None):\n        try:\n            th_opt = self._theme['theme']['options']\n            if color == 'white' : color = (249, 249, 249, 255)\n            if color == 255 : color = 'black'\n            if text is not None and tfont is not None:\n                try:\n                    w,h = tfont.getsize(text)\n                except:\n                    _,_,w,h = tfont.getbbox(text)\n                nb_lines = text.count('\\n') + 1\n                h = (h + 1) * nb_lines\n                if nb_lines > 1:\n                    lines = text.split('\\n')\n                    max_char = 0\n                    tot_char = 0\n                    for line in lines:\n                        tot_char = tot_char + len(line)\n                        char_line = len(line)\n                        if char_line > max_char: max_char = char_line\n                    w = int(w / (tot_char / max_char))\n                imgtext = Image.new('1', (w,h), 0xff)\n                dt = ImageDraw.Draw(imgtext)\n                dt.text((0,0), text, font=tfont, fill=0x00)\n                if color == 0: color = 'black'\n                imgtext = ImageOps.colorize(imgtext.convert('L'), black = color, white = 'white')\n                imgtext = imgtext.convert(\"RGBA\")\n                data = imgtext.getdata()\n                newData = []\n                for item in data:\n                    if item[0] in range(250, 256) and item[1] in range(250, 256) and item[2] in range(250, 256):\n                        newData.append((255, 255, 255, 0))\n                    else:\n                        newData.append(item)\n                imgtext.putdata(newData)\n                imgtext = imgtext.convert('RGBA')\n                return imgtext\n        except Exception as e:\n            self.log(f\"Error while rgba_text ({text}; {tfont}; {color}; {width}; {height}): {str(e)}\")\n            self.log(traceback.format_exc())\n            return None\n\n    def add_widget(self, ui, key, widget_type, th_widget):\n        conf = 0\n        th_opt = self._theme['theme']['options']\n        if key in self._state:\n\n\n\n            if key in th_widget:\n                if self.orientation == 'vertical' and th_widget[key].get('position-v'):\n                    if self._state[key]['position'] != tuple(th_widget[key]['position-v']):\n                        self._state[key]['position'] =  tuple(th_widget[key]['position-v'])\n                elif th_widget[key].get('position'):\n                    if self._state[key]['position'] != tuple(th_widget[key]['position']):\n                        self._state[key]['position'] =  tuple(th_widget[key]['position'])\n            else:\n                if self._state[key]['position'] != tuple(ui._state.get_attr(key, 'xy')):\n                    position = ui._state.get_attr(key, 'xy')\n                    position = tuple(int(coord) for coord in position)\n                    if len(position) >= 3:\n                        position = box_to_xywh(position)\n                    self._state[key]['position'] = position\n\n\n\n            if key in th_widget and th_widget[key].get('color'):\n                if self._state[key]['color'] != th_widget[key]['color']:\n                    self._state[key]['color'] = th_widget[key]['color']\n                    self._state[key]['icolor'] = 0\n            elif \"base_text_color\" in th_opt and th_opt['base_text_color']:\n                if self._state[key]['color'] != th_opt['base_text_color']:\n                    self._state[key]['color'] = th_opt['base_text_color']\n                    self._state[key]['icolor'] = 0\n            else:\n                if self._state[key]['color'] != [ui._state.get_attr(key, 'color')]:\n                    self._state[key]['color'] = [ui._state.get_attr(key, 'color')]\n                    self._state[key]['icolor'] = 0\n\n        elif key not in self._state:\n            conf = 1\n            self._state[key] = {}\n            self._state_default[key] = {}\n            self._state_default[key] = copy.deepcopy(self._state[key])\n            default_values = self.widget_defaults.get(widget_type, {})\n            self._state[key] = copy.deepcopy(default_values)\n            self._state[key].update({'widget_type': widget_type})\n            self._state_default[key].update({'widget_type': widget_type})\n\n            position = ui._state.get_attr(key, 'xy')\n            position = tuple(int(coord) for coord in position)\n            \n            if len(position) >= 3:\n                position = box_to_xywh(position)\n            self._state[key].update({'position': position})\n            self._state_default[key].update({'position': position})\n\n            if key in th_widget:\n                \n                if self.orientation == 'vertical':\n                    if th_widget[key].get('position-v'):\n                        self._state[key].update({'position': tuple(th_widget[key]['position-v'])})\n                    elif th_widget[key].get('position'):\n                        self._state[key].update({'position': tuple(th_widget[key]['position'])})\n                elif th_widget[key].get('position'):\n                    self._state[key].update({'position': tuple(th_widget[key]['position'])})\n            self._state_default[key].update({'color': [ui._state.get_attr(key, 'color')]})\n            if key in th_widget and th_widget[key].get('color'):\n                self._state[key].update({'color': th_widget[key]['color']})\n                self._state[key].update({'icolor': 0})\n            elif th_opt.get('base_text_color'):\n                self._state[key].update({'color': th_opt['base_text_color']})\n                self._state[key].update({'icolor': 0})\n            else:\n                self._state[key].update({'color': [ui._state.get_attr(key, 'color')]})\n                self._state[key].update({'icolor': 0})\n            if key in th_widget and 'z_axis' in th_widget[key]:\n                self._state[key].update({'z_axis': th_widget[key]['z_axis']})\n            if widget_type == 'Text' or widget_type == 'LabeledValue':\n                if key in th_widget and th_widget[key].get('text_font'):\n                    if th_widget[key].get('text_font') == '' or th_widget[key].get('text_font') == None:\n                        self._state[key].pop('text_font', None)\n                    else:\n                        self._state[key].update({'text_font': th_widget[key]['text_font']})\n                if widget_type == 'Text':\n                    self._state_default[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'font'))})\n                if widget_type == 'LabeledValue':\n                    self._state_default[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'text_font'))})\n                if key in th_widget and th_widget[key].get('text_font_size'):\n                    self._state[key].update({'text_font_size': th_widget[key]['text_font_size']})\n                else:\n                    if widget_type == 'Text':\n                        self._state[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'font'))})\n                    if widget_type == 'LabeledValue':\n                        self._state[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'text_font'))})\n                if key in th_widget and th_widget[key].get('size_offset'):\n                    self._state[key].update({'size_offset': th_widget[key]['size_offset']})\n                if key in th_widget and th_widget[key].get('icon'):\n                    self._state[key].update({'icon': th_widget[key]['icon']})\n                if key in th_widget and th_widget[key].get('icon_color'):\n                    self._state[key].update({'icon_color': th_widget[key]['icon_color']})\n                if key in th_widget and th_widget[key].get('invert'):\n                    self._state[key].update({'invert': th_widget[key]['invert']})\n                if key in th_widget and th_widget[key].get('alpha'):\n                    self._state[key].update({'alpha': th_widget[key]['alpha']})\n                if key in th_widget and th_widget[key].get('crop'):\n                    self._state[key].update({'crop': th_widget[key]['crop']})\n                if key in th_widget and th_widget[key].get('mask'):\n                    self._state[key].update({'mask': th_widget[key]['mask']})\n                if key in th_widget and th_widget[key].get('refine'):\n                    self._state[key].update({'refine': th_widget[key]['refine']})\n                if key in th_widget and th_widget[key].get('zoom'):\n                    self._state[key].update({'zoom': th_widget[key]['zoom']})\n                if key in th_widget and th_widget[key].get('image_type'):\n                    self._state[key].update({'image_type': th_widget[key]['image_type']})\n            if widget_type == 'Text':\n                self._state_default[key].update({'wrap': ui._state.get_attr(key, 'wrap')})\n                if key in th_widget and th_widget[key].get('wrap'):\n                    self._state[key].update({'wrap': th_widget[key]['wrap']})\n                else:\n                    self._state[key].update({'wrap': ui._state.get_attr(key, 'wrap')})\n                self._state[key].update({'max_length': ui._state.get_attr(key, 'max_length')})\n                if key in th_widget and th_widget[key].get('max_length'):\n                    self._state[key].update({'max_length': th_widget[key]['max_length']})\n                else:\n                    self._state[key].update({'max_length': ui._state.get_attr(key, 'max_length')})\n                if key in th_widget and th_widget[key].get('face'):\n                    self._state[key].update({'max_length': th_widget[key]['max_length']})\n            if widget_type == 'LabeledValue':\n                self._state_default[key].update({'label': ui._state.get_attr(key, 'label')})\n                if key in th_widget and 'label' in th_widget[key]:\n                    self._state[key].update({'label': th_widget[key]['label']})\n                else:\n                    self._state[key].update({'label': ui._state.get_attr(key, 'label')})\n                if key in th_widget and th_widget[key].get('label_font'):\n                    self._state[key].update({'label_font': th_widget[key]['label_font']})\n                if key in th_widget and th_widget[key].get('label_font_size'):\n                    self._state[key].update({'label_font_size': th_widget[key]['label_font_size']})\n                else:\n                    self._state[key].update({'label_font_size': verify_font_info(ui._state.get_attr(key, 'label_font'))})\n                self._state_default[key].update({'label_spacing': ui._state.get_attr(key, 'label_spacing')})\n                if key in th_widget and th_widget[key].get('label_spacing'):\n                    self._state[key].update({'label_spacing': th_widget[key]['label_spacing']})\n                elif 'label_spacing' in th_opt and th_opt['label_spacing']:\n                    self._state[key].update({'label_spacing': th_opt['label_spacing']})\n                else:\n                    self._state[key].update({'label_spacing': ui._state.get_attr(key, 'label_spacing')})\n                if key in th_widget and th_widget[key].get('label_line_spacing'):\n                    self._state[key].update({'label_line_spacing': th_widget[key]['label_line_spacing']})\n                elif 'label_line_spacing' in th_opt and th_opt['label_line_spacing']:\n                    self._state[key].update({'label_line_spacing': th_opt['label_line_spacing']})\n                else:\n                    self._state[key].update({'label_line_spacing': 0})\n\n                if key in th_widget and th_widget[key].get('f_awesome'):\n                    self._state[key].update({'f_awesome': th_widget[key]['f_awesome']})\n                if key in th_widget and th_widget[key].get('f_awesome_size'):\n                    self._state[key].update({'f_awesome_size': th_widget[key]['f_awesome_size']})\n            if widget_type == 'Line':\n                self._state_default[key].update({'width': ui._state.get_attr(key, 'width')})\n                if key in th_widget and th_widget[key].get('width'):\n                    self._state[key].update({'width': th_widget[key]['width']})\n                else:\n                    self._state[key].update({'width': ui._state.get_attr(key, 'width')})\n            if widget_type == 'Bitmap':\n                self._state[key].update({'f_awesome': False})\n                if key in th_widget and th_widget[key].get('icon'):\n                    self._state[key].update({'icon': th_widget[key]['icon']})\n                if key in th_widget and th_widget[key].get('invert'):\n                    self._state[key].update({'invert': th_widget[key]['invert']})\n                if key in th_widget and th_widget[key].get('alpha'):\n                    self._state[key].update({'alpha': th_widget[key]['alpha']})\n                if key in th_widget and th_widget[key].get('crop'):\n                    self._state[key].update({'crop': th_widget[key]['crop']})\n                if key in th_widget and th_widget[key].get('mask'):\n                    self._state[key].update({'mask': th_widget[key]['mask']})\n                if key in th_widget and th_widget[key].get('refine'):\n                    self._state[key].update({'refine': th_widget[key]['refine']})\n                if key in th_widget and th_widget[key].get('zoom'):\n                    self._state[key].update({'zoom': th_widget[key]['zoom']})\n                if key in th_widget and th_widget[key].get('icon_color'):\n                    self._state[key].update({'icon_color': th_widget[key]['icon_color']})\n\n        if conf:\n            self.configure_widget(ui, key, widget_type)\n    \n    def get_face_path(self, img_path, face, image_type):\n        variations = [\n            face, \n            face.upper(),\n            face.lower(),\n            face.capitalize(),\n            face.replace('-', '_'),\n            face.replace('_', '-'),\n            face.replace('-', '_').upper(),\n            face.replace('_', '-').upper(),\n            face.replace('-', '_').lower(),\n            face.replace('_', '-').lower()\n        ]\n\n        for variation in variations:\n            face_path = os.path.join(img_path, f'{variation}.{image_type}')\n            if os.path.exists(face_path):\n                return face_path\n\n        return None\n\n    def configure_widget(self, ui, key, widget_type):\n        try:\n            if key == 'face':\n                self._state[key].update({'face': True})\n                self._state[key].update({'f_awesome': False})\n                if self._state[key]['icon']:\n                    face_dict = self._config['ui']['faces']\n                    img_path = os.path.join(self._th_path, 'img', 'face')\n                    for face in face_dict:\n                        image_type = self._state[key].get('image_type', 'png')\n                        if isinstance(face_dict[face], str):\n                            face_path = self.get_face_path(img_path, face, image_type)\n                            if face_path:\n                                if 'face_map' not in self._state[key]:\n                                    self._state[key].update({'face_map': {}})\n                                self._state[key]['face_map'].update({\n                                    face: [\n                                        face_dict[face],\n                                        adjust_image(face_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])\n                                    ]\n                                })\n                            else:\n                                logging.warning(f\"[Fancygotchi] No valid face path found for '{face}'\")\n                     \n            if key == 'friend_face':\n                self._state[key].update({'friend_face': True})\n                face_dict = self._config['ui']['faces']\n                self._state[key].update({'f_awesome': False})\n                if self._state[key]['icon']:\n                    face_dict = self._config['ui']['faces']\n                    img_path = os.path.join(self._th_path, 'img', 'friend_face')\n                    for face in face_dict:\n                        image_type = self._state[key].get('image_type', 'png')\n                        if isinstance(face_dict[face], str):\n                            face_path = self.get_face_path(img_path, face, image_type)\n                            if face_path:\n                                if 'friend_face_map' not in self._state[key]:\n                                    self._state[key].update({'friend_face_map': {}})\n                                self._state[key]['friend_face_map'].update({\n                                    face: [\n                                        face_dict[face],\n                                        adjust_image(face_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])  \n                                    ]\n                                })\n                            else:\n                                print(f\"Warning: No valid face path found for '{face}'\")\n\n            if self._state[key].get('icon'):\n                if self._state[key]['icon'] == True:\n                    source = 'label'\n                    if key not in ['face', 'friend_face']:\n                        if widget_type == 'LabeledValue' and not self._state[key]['f_awesome']:\n                            \n                            icon_path = os.path.join(self._th_path, 'img', 'widgets', key, self._state[key][source])\n                            self._state[key].update({'icon_image': adjust_image(Image.open(icon_path), self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])})\n                        if not self._state[key]['f_awesome']:\n                            if widget_type == 'Bitmap':\n                                img_path = os.path.join(self._th_path, 'img', 'widgets', key)\n                                files = [f for f in os.listdir(img_path)]\n                                file_count = len(files)\n                                if file_count == 1:\n                                    image_path = os.path.join(img_path, files[0])\n                                    self._state[key].update({'image': adjust_image(image_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])})\n\n                                elif file_count > 3 and file_count % 2 == 0:\n                                    image_dict = {}\n                                    file_names = [os.path.splitext(f)[0] for f in files] \n\n                                    for file in file_names:\n                                        if file.endswith('A'):\n                                            id_number = file[:-1] \n                                            corresponding_b = id_number + 'B' \n                                            \n                                            if corresponding_b in file_names:\n                                                original_a = [f for f in files if os.path.splitext(f)[0] == file][0]\n                                                original_b = [f for f in files if os.path.splitext(f)[0] == corresponding_b][0]\n                                                \n                                                img_a = Image.open(os.path.join(img_path, original_a))\n                                                img_b = adjust_image(Image.open(os.path.join(img_path, original_b)), self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])\n                                                \n                                                image_dict[int(id_number)] = [img_a, img_b]\n                                    self._state[key].update({'image_dict': image_dict})\n                                    img_o, img_c = image_dict[2]\n                                    self._state[key].update({'image': img_c})\n                                else:\n                                    self.log(f\"Error: There are {file_count} images.\")\n                                    icon_img = Image.new('1', (10, 10), 0x00)\n                                    self._state[key].update({'image': icon_img})\n                        else:\n                            fa_path = os.path.join(self._th_path, 'fonts', self.f_awesome_name)\n                            fa = ImageFont.truetype(fa_path, self._state[key]['f_awesome_size'])\n                            try:\n                                code_point = int(self._state[key][source], 16)\n                            except:\n                                self.log(\"wrong font awesome icon code point: %s\" % self._state[key][source])\n                                code_point = int(\"f00d\", 16)\n                            icon = chr(code_point)\n                            w,h = fa.getsize(icon)\n                            icon_img = Image.new('1', (int(w), int(h)), 0xff)\n                            dt = ImageDraw.Draw(icon_img)\n                            dt.text((0,0), icon, font=fa, fill=0x00)\n                            icon_img = icon_img.convert('RGBA')\n                            self._state[key].update({'icon_image': icon_img})\n        except Exception as e:\n            self.log(\"non fatal error while configuring Fancygotchi widget: %s\" % e)\n            self.log(traceback.format_exc())\n\n    def remove_widgets(self, ui):\n            if self._state:\n                keys_to_delete = [] \n                for key, state in self._state.items():\n                    tag = 0\n                    for k, s in ui._state.items():\n                        if key == k:\n                            tag = 1\n                    if tag == 0:\n                        keys_to_delete.append(key)\n                for key in keys_to_delete:\n                    self.log(f'remove widget: {key}')\n                    del self._state[key]\n    \n    def pwncanvas_creation(self, res):\n        try:\n            th_opt = self._theme['theme']['options']\n            rot = self.options['rotation']\n            if rot == 0 or rot == 180:\n                self.orientation = 'horizontal'\n                x, y = res\n                l = x\n                w = y\n            elif rot == 90 or rot == 270:\n                self.orientation = 'vertical'\n                x, y = res\n                l = y\n                w = x\n\n            bg_color = th_opt['bg_color']\n            if isinstance(bg_color, list):\n                bg_color = tuple(bg_color)\n\n            if not bg_color or bg_color == '':\n                bg_color = (0,0,0,0)\n            \n            self._pwncanvas = Image.new('RGBA', (l, w), bg_color)\n            self._pwndata = Image.new('RGBA', (l, w), (0,0,0,0))\n\n            if not self._frames == []:\n                iframe = self._frames[self._i]\n                self._pwncanvas.paste(iframe, (0,0), iframe)\n\n            if isinstance(self._bg, Image.Image) and self._bg is not None:\n                self._pwncanvas.paste(self._bg, (0,0), self._bg.convert('RGBA'))\n\n        except Exception as e:\n            logging.error(f\"Error in pwncanvas_creation: {e}\")\n            raise \n\n    def pos_convert(self, x, y, w, h, r=None, r0=None, r1=None):\n        rot = self._config.get('main',{}).get('plugins',{}).get('Fancygotchi',{}).get('rotation', 0)\n        if r is not None:\n            rot = r\n        if rot == 0 or rot == 180:\n            if r0 is not None: width = r0\n            else: width = self._res[0]\n            if r1 is not None: height = r1\n            else: height = self._res[1]\n        if rot == 90 or rot == 270:\n            width = self._res[1]\n            height = self._res[0]\n            if r1 is not None: width = r1\n            else: width = self._res[0]\n            if r0 is not None: height = r0\n            else: height = self._res[1]\n        if isinstance(w, str) and '%' in w:\n            try:\n                percent_value = float(w.replace('%', ''))\n                w = (percent_value / 100) * width\n            except ValueError:\n                self.log(f\"Invalid percentage value for width: {w}\")\n                w = 0  \n        else:\n            w = int(w)\n\n        if isinstance(h, str) and '%' in h:\n            try:\n                percent_value = float(h.replace('%', ''))\n                h = (percent_value / 100) * height\n            except ValueError:\n                self.log(f\"Invalid percentage value for height: {h}\")\n                h = 0  \n        else:\n            h = int(h)\n        top = 0\n        bottom = height - h\n        right = width - w\n        left = width\n        center_x = (width / 2) - (w / 2)\n        center_y = (height / 2) - (h / 2)\n        \n        def replace_keywords(formula, axis):\n            keyword_mapping = {\n                \"center_x\": center_x,\n                \"center_y\": center_y,\n                \"left\": left,\n                \"right\": right,\n                \"top\": top,\n                \"bottom\": bottom,\n                \"width\": width,\n                \"height\": height,\n                \"w\": w,\n                \"h\": h,\n            }\n\n            if axis == 'x':\n                keyword_mapping[\"center\"] = center_x\n                keyword_mapping.pop('center_y', None)\n                keyword_mapping.pop('top', None)\n                keyword_mapping.pop('bottom', None)\n                keyword_mapping.pop('height', None)\n            elif axis == 'y':\n                keyword_mapping[\"center\"] = center_y\n                keyword_mapping.pop('center_x', None)\n                keyword_mapping.pop('left', None)\n                keyword_mapping.pop('right', None)\n                keyword_mapping.pop('width', None)\n            else:\n                raise ValueError(\"Invalid axis. Choose 'x' or 'y'.\")\n\n            for keyword, value in keyword_mapping.items():\n                if keyword in formula:\n                    formula = formula.replace(keyword, str(value))\n\n            return formula\n\n        def safe_eval(expr):\n            try:\n                if re.search(r'[^0-9\\+\\-\\*/\\(\\)\\. ]', expr):\n                    raise ValueError(f\"Invalid expression: {expr}\")\n                result = eval(expr)\n                return result\n            except Exception as e:\n                self.log(f\"Error evaluating expression: {expr}. Exception: {e}\")\n                return 0\n\n        axis = 'x'\n        if not is_int(x):\n            try:\n                x = replace_keywords(x, axis)\n                x = safe_eval(x) \n            except ValueError as e:\n                self.log(f\"Error processing x: {e}\")\n                x = 0\n        else:\n            x = int(x)\n            if x < 0:\n                x = width + x\n\n        axis = 'y'\n        if not is_int(y):\n            try:\n                y = replace_keywords(y, axis)\n                y = safe_eval(y) \n            except ValueError as e:\n                self.log(f\"Error processing y: {e}\")\n                y = 0\n        else:\n            y = int(y)\n            if y < 0:\n                y = height + y\n\n        x2 = int(x + w)\n\n        y2 = int(y + h)\n\n        return int(x), int(y), int(x2), int(y2)\n    \n    def paste_image(self, img, x, y):\n        \n        if isinstance(img, Image.Image):\n            w, h = img.size\n            x, y, x2, y2 = self.pos_convert(x,y,w,h)\n            img = img.convert('RGBA') \n            self._pwndata.paste(img, (x, y, x2, y2), img)\n\n            x, y ,x2 ,y2 = self.pos_convert(x, y, w, h)\n\n    def paste_value(self, value, pos, text_font, color, wrap=None):\n        x, y = pos\n\n        if wrap and hasattr(self, 'wrapper') and self.wrapper is not None:\n            try:\n                text = '\\n'.join(self.wrapper.wrap(value))\n            except AttributeError:\n                text = value\n        else:\n            text = value\n\n        imgtext = self.rgba_text(text, text_font, color, self._res[0], self._res[1])\n        self.paste_image(imgtext, x, y)\n\n    def drawer(self):\n        try:\n            th_opt = copy.deepcopy(self._default['theme']['options'])\n            th_opt.update( self._theme['theme']['options'])\n            draw = ImageDraw.Draw(self._pwndata)\n            draw_state = dict(sorted(self._state.items(), key=lambda item: item[1].get('z_axis', 0)))\n            \n            keys_to_remove = {key: value for key, value in draw_state.items() if value.get('z_axis', 0) < 0}\n            for key in keys_to_remove:\n                del draw_state[key]\n\n            if self.stealth_mode:\n                keys_to_remove = {key for key, value in draw_state.items() if value.get('z_axis', 0) < 100}\n                for key in keys_to_remove:\n                    del draw_state[key]\n            for widget, state in draw_state.items():\n                if 'wrap' in state:\n                    wrap = state['wrap']\n                else:\n                    wrap = False\n                if 'main_text_color' in th_opt and th_opt['main_text_color'] in ([], \"\"):\n                    if 'color' in state:\n                        color = state['color'][state['icolor']]\n                    else:\n                        color = ['black']\n                elif 'main_text_color' in th_opt and th_opt['main_text_color'] != []:\n                    color = th_opt['main_text_color'][self._icolor]\n\n                if len(state['position']) >= 3:\n                    x, y, w, h = state['position']\n                    x, y, x2, y2 = self.pos_convert(x,y,w,h)\n                if len(state['position']) == 2:\n                    x, y = state['position']\n                self.wrapper = TextWrapper(width=state['max_length'], replace_whitespace=False) if wrap else None\n\n                if state['widget_type'] == 'Text' or state['widget_type'] == 'LabeledValue':\n                    if state['widget_type'] == 'LabeledValue':\n                        try:\n                            label_font = getattr(self, state[\"label_font_size\"])\n                            label = state['label']\n\n                        except Exception as e:\n                            label_font = getattr(self, 'Medium')\n\n                    try:\n                        text_font = getattr(self, state[\"text_font_size\"]) \n                    except Exception as e:\n                        text_font = getattr(self, 'Medium')\n\n                    \n                    if 'text_font' in state or 'size_offset' in state:\n                        if 'text_font' in state and state['text_font']:\n                            font = state['text_font']\n                        else:\n                            font = th_opt['status_font']\n\n                        if 'size_offset' in state:\n                            size_offset = state['size_offset']\n                        else:\n                            size_offset = th_opt['size_offset']\n                        if text_font is not None:\n                            text_font = self.change_font(text_font, font, size_offset)\n\n                    if state['widget_type'] == 'LabeledValue':\n                        if label_font is not None and state['label'] is not None:\n                            try:\n                                lw, lh = label_font.getsize(label)\n                            except:\n                                _, _, lw, lh = label_font.getbbox(state['label'])\n                        else:\n                            lw, lh = 0, 0\n                    \n                    if text_font is not None and state['value'] is not None:\n                        try:\n                            vw, vh = text_font.getsize(state['value'])\n                        except:\n                            _, _, vw, vh = text_font.getbbox(state['value'])\n                    else:\n                        vw, vh = 0, 0\n\n                    if state['widget_type'] == 'LabeledValue':\n                        total_height = max(lh,vh)+max(0,state['label_line_spacing'])\n                        total_width = lw + state['label_spacing'] + 5 * len(state['label'])\n                        x, y, l_w, l_h = self.pos_convert(x, y, total_width, total_height)\n                        v_y = y + state['label_line_spacing']\n                        v_x = x + state['label_spacing'] + 5 * len(state['label'])\n                    elif state['widget_type'] == 'Text':\n                        v_x, v_y, v_w, v_h = self.pos_convert(x, y, vw, vh)\n\n                    if state['value'] is not None:\n                        if not state['icon']:\n                            self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)\n                            if state['widget_type'] == 'LabeledValue':\n                                l_text = state['label']\n                            if state['widget_type'] == 'LabeledValue':\n                                imgtext = self.rgba_text(l_text, label_font, color, self._res[0], self._res[1])\n                                self.paste_image(imgtext, x, y)\n                        else:\n                            if 'f_awesome' in state and state['f_awesome'] == False:\n                                if widget not in ['face', 'friend_face']:\n                                    self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)\n                                    icon_image = state['icon_image']\n                                else:\n                                    if widget == 'face':\n                                        x = v_x\n                                        y = v_y\n                                        for face in state['face_map'].items():\n                                            face_name, face_map = face\n                                            if face_map[0] == state['value']:\n                                                icon_image = face_map[1]\n                                    elif widget == 'friend_face':\n                                        x = v_x\n                                        y = v_y\n                                        for face in state['friend_face_map'].items():\n                                            friend_face_name, friend_face_map = face\n                                            if friend_face_map[0] == state['value']:\n                                                icon_image = friend_face_map[1]\n\n                                if 'icon_color' in state and state['icon_color']:\n                                    alpha = 0\n                                    wht = (255, 255, 255, 255)\n                                    if color == 'white': color = (249,249,249,256)\n                                    if icon_image.mode in ('RGBA', 'LA') or (icon_image.mode == 'P' and 'transparency' in icon_image.info):\n                                        alpha = 1\n                                        white_image = Image.new('RGB', icon_image.size, wht)\n                                        white_image.paste(icon_image, mask=icon_image.split()[3])\n                                        icon_image = white_image\n                                    L_image = icon_image.convert('L')\n                                    icon_image = ImageOps.colorize(L_image, black = color, white = wht)\n                                    if alpha:\n                                        icon_image = icon_image.convert('RGBA')\n                                        data = icon_image.getdata()\n                                        newData = []\n                                        for item in data:\n                                            if item[0] in range(250, 256) and item[1] in range(250, 256) and item[2] in range(250, 256):\n                                                newData.append((255, 255, 255, 0))\n                                            else:\n                                                newData.append(item)\n                                        icon_image.putdata(newData)\n                                        icon_image = icon_image.convert('RGBA')\n                                self.paste_image(icon_image, x, y)\n\n                            else:\n                                if color == 'white': color = (249,249,249,255)\n                                img = state['icon_image'].convert('L')\n                                icon_image = ImageOps.colorize(img, black = color, white = 'white')\n                                icon_image = icon_image.convert('RGBA')\n                                data = icon_image.getdata()\n                                newData = []\n                                for item in data:\n                                    if item[0] in range(240, 256) and item[1] in range(240, 256) and item[2] in range(240, 256):\n                                        newData.append((255, 255, 255, 0))\n                                    else:\n                                        newData.append(item)\n                                icon_image.putdata(newData)\n                                icon_image = icon_image.convert('RGBA')\n                                self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)\n                                self.paste_image(icon_image, x, y)\n\n                elif state['widget_type'] == 'Bitmap':\n                    icon_bmp = state['image']\n                    alpha = 0\n                    original = icon_bmp\n                    iw, ih = icon_bmp.size\n                    v_x, v_y, v_x2, v_y2 = self.pos_convert(state['position'][0], state['position'][1], iw, ih)\n                    if icon_bmp is not None:\n                        if icon_bmp.mode in ('RGBA', 'LA') or (icon_bmp.mode == 'P' and 'transparency' in icon_bmp.info):\n                            alpha = 1\n                            if 'mask' in state and state['mask']:\n                                refine = state['refine']\n                                image = icon_bmp.convert('RGBA')\n                                \n                                width, height = image.size\n                                pixels = image.getdata()\n                                new_pixels = []\n                                \n                                for pixel in pixels:\n                                    r, g, b, a = pixel\n                                    \n                                    if r > 255 - refine and g > 255 - refine and b > 255 - refine:\n                                        new_pixel = (255, 255, 255, 0)  \n                                    else:\n                                        new_pixel = (r, g, b, a)\n                                    \n                                    new_pixels.append(new_pixel)\n                                \n                                refined_image = Image.new(\"RGBA\", image.size)\n                                refined_image.putdata(new_pixels)\n                                white_image = Image.new('RGB', refined_image.size, (255, 255, 255))\n                                white_image.paste(icon_bmp, mask=refined_image.split()[3])\n                                icon_bmp = white_image\n                        if 'icon_color' in state and state['icon_color']:\n                            L_image = icon_bmp.convert('L')\n                            icon_bmp = ImageOps.colorize(L_image, black = color, white = (255, 255, 255))\n                        if 'alpha' in state and state['alpha']:\n                            if alpha:\n                                icon_bmp = alphamask(icon_bmp)\n                        self.paste_image(icon_bmp, v_x, v_y)\n                elif state['widget_type'] == 'Line':\n                    draw.line([x,y,x2,y2], fill=color, width=state['width'])\n                elif state['widget_type'] == 'Rect':\n                    draw.rectangle([x,y,x2,y2], fill=color)\n                elif state['widget_type'] == 'FilledRect':\n                    draw.rectangle([x,y,x2,y2], fill=color)\n\n                if state['icolor'] + 1 >= len(state['color']):\n                    state['icolor'] = 0\n                else:\n                    state['icolor'] += 1\n\n            if self._icolor + 1 >= len(th_opt['main_text_color']):\n                self._icolor = 0\n            else:\n                self._icolor += 1  \n\n            if hasattr(self, 'fancy_menu'):\n                if getattr(self.fancy_menu, 'active', False): \n                    menu_img = self.fancy_menu.render()\n                    self.fancy_menu_img = menu_img\n                    if self.fancy_menu_img is not None:\n                        self._pwndata.paste(self.fancy_menu_img, (0, 0, self._pwndata.size[0], self._pwndata.size[1]), self.fancy_menu_img.split()[3])\n\n            if self._fg != '':\n                target_width = self._pwndata.size[0]\n                target_height = self._pwndata.size[1]\n                self._fg = self._fg.resize((target_width, target_height), Image.Resampling.LANCZOS)\n                self._pwndata.paste(self._fg, (0, 0), self._fg)\n            self._pwncanvas = self._pwncanvas.convert(\"RGB\")\n            self._pwncanvas.paste(self._pwndata, (0, 0, self._pwndata.size[0], self._pwndata.size[1]),self._pwndata)\n            self._pwncanvas = self._pwncanvas.convert(\"RGBA\")\n\n        except Exception as e:\n            logging.error(f\"Error in Fancygotchi drawer: {e}\")\n            logging.error(traceback.format_exc())\n            raise\n\n    def ui2(self):\n        try:\n            image = self.second_screen\n            if hasattr(self, 'display_controller') and self.display_config['second_screen_webui'] and self.dispHijack:\n                image = self.display_controller.screen()\n            img_io = BytesIO()\n            image.save(img_io, 'PNG')\n            img_io.seek(0) \n            return send_file(img_io, mimetype='image/png'), 200\n\n        except Exception as ex:\n            image = self.second_screen\n            img_io = BytesIO()\n            image.save(img_io, 'PNG')\n            img_io.seek(0) \n            return send_file(img_io, mimetype='image/png'), 200\n\n    def on_webhook(self, path, request):\n        try:\n            if not self.ready:\n                return \"Plugin not ready\"\n            if request.method == \"GET\":\n                if path == \"/\" or not path:\n                    themes = sorted(self.theme_list(), key=lambda x: x.lower())\n                    if self._theme_name != 'Default':\n                        css_path = os.path.join(self._th_path, 'style.css')\n                        info_path = os.path.join(self._th_path, 'info.json')\n                        rot = self._config['main']['plugins']['Fancygotchi']['rotation']\n                        \n                        if self.cfg_path is not None:\n                            cfg_path = self.cfg_path\n                        else:\n                            cfg_path = 'No custom configuration'\n\n                        name = self._theme_name\n                        if os.path.exists(css_path):\n                            css = open(css_path, 'r').read()\n                        else:\n                            css = ''\n                            css_path = 'No custom css'\n\n                        if os.path.exists(info_path):\n                            info = open(info_path, 'r').read()\n                        else:\n                            info = ''\n                            info_path = 'No custom info'\n\n                    else:\n                        css_path = 'default has no custom css'\n                        info_path = 'default has no custom info'\n                        cfg_path = 'default has no custom configuration'\n                        css = ''\n                        info = ''\n                        name = 'Default'\n                    files = {'CSS': [css_path, css], 'Info': [info_path, info]}\n                    return render_template_string(\n                        INDEX, themes=themes, \n                        default_theme=name, \n                        rotation=self._config['main']['plugins']['Fancygotchi']['rotation'],\n                        author=Fancygotchi.__author__,\n                        version=Fancygotchi.__version__,\n                        files=files,\n                        cfg_path=cfg_path,\n                        name=name,\n                        logo=LOGO,\n                        webui_fps=self.webui_fps,\n                        fancy_repo=FANCY_REPO,\n                    )\n                elif path == \"key\":\n                    # curl -X GET \"http://changeme:changeme@localhost:8080/plugins/Fancygotchi/key\"\n                    return render_template_string(\"{{ csrf_token() }}\"), 200\n                elif path == \"ui2\":\n                    return self.ui2()\n                elif path == \"active_theme\":\n                    return json.dumps({\"theme\": self._theme_name})\n                elif path == \"theme_list\":\n                    themes = self.theme_list()\n                    return json.dumps(themes)\n                elif path == \"theme_download_list\":\n                    self.log(\"Theme download list fetching started...\")\n                    try:\n                        isInternet, msg = check_internet_and_repo()\n                        self.log(f\"isInternet: {isInternet}, msg: {msg}\")\n                        if isInternet:\n\n                            themes_dict = self.fetch_themes()\n                            return json.dumps({\"status\": 200, \"data\": themes_dict}), 200\n                        else:\n                            return json.dumps({\"error\": msg}), 500\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return json.dumps({\"error\": \"Theme download list error\"}), 500           \n                elif str(path).split(\"/\")[0] == \"theme_export\":\n                    try: \n                        theme_name = path.split(\"/\")[-1]\n                        return self.theme_export(theme_name)\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"theme selection error\", 500\n                elif path == \"load_config\":\n                    try:\n                        if self.cfg_path is not None:\n                            cfg_path = self.cfg_path\n                            with open(cfg_path, 'r') as f:\n                                config = toml.load(f)\n                        else:\n                            config = {}\n                            cfg_path = \"\"\n                        if self._theme_name != 'Default':\n                            css_path = os.path.join(self._th_path, 'style.css')\n                            info_path = os.path.join(self._th_path, 'info.json')\n\n                            if os.path.exists(css_path):\n                                with open(css_path, 'r') as f:\n                                    css_content = f.read()\n                            else:\n                                css_content = 'No custom CSS'\n\n                            if os.path.exists(info_path):\n                                with open(info_path, 'r') as f:\n                                    info_content = f.read()\n                            else:\n                                info_content = 'No custom Info'\n                        else:\n                            css_content = 'No custom CSS'\n                            info_content = 'No custom Info'\n                            css_path = 'No custom CSS path'\n                            info_path = 'No custom Info path'\n\n                        return json.dumps({\n                            \"config\": config,\n                            \"css\": css_content,\n                            \"info\": info_content,\n                            \"name\": self._theme_name,\n                            \"cfg_path\": cfg_path,\n                            \"css_path\": css_path,\n                            \"info_path\": info_path,\n                        })\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Error loading configuration\", 500\n                elif path == \"display_hijack\":\n                    try:\n                        self.dispHijack = True\n                        return json.dumps({\"message\": \"Hijack display successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Display hijacking error\", 500           \n                elif path == \"display_pwny\":\n                    try:\n                        self.dispHijack = False\n                        return json.dumps({\"message\": \"Pwny change successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Display Pwny error\", 500\n                elif path == \"second_screen\":\n                    logging.warning(\"second_screen\")\n                    try:\n                        self.dispHijack = not self.dispHijack\n                        return json.dumps({\"message\": \"Second screen change successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Display Pwny error\", 500\n                elif path == \"display_next\":\n                    try:\n                        self.process_actions({\"action\": \"switch_screen_mode\"})\n                        return json.dumps({\"message\": \"Display change successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Display next error\", 500\n                elif path == \"display_previous\":\n                    try:\n                        self.process_actions({\"action\": \"switch_screen_mode_reverse\"})\n                        return json.dumps({\"message\": \"Display change successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Display previous error\", 500\n                elif path == \"screen_saver_next\":\n                    try:\n                        self.process_actions({\"action\": \"next_screen_saver\"})\n                        return json.dumps({\"message\": \"Screen saver change successful!\", \"status\": 200})\n                    except Exception as ex:\n                            logging.error(ex)\n                            logging.error(traceback.format_exc())\n                            return \"Next screen saver error\", 500\n                elif path == \"screen_saver_previous\":\n                    try:\n                        self.process_actions({\"action\": \"previous_screen_saver\"})\n                        return json.dumps({\"message\": \"Screen saver change successful!\", \"status\": 200})\n                    except Exception as ex:\n                            logging.error(ex)\n                            logging.error(traceback.format_exc())\n                            return \"previous screen saver error\", 500\n                elif path == \"stealth\":\n                    try:\n                        self.process_actions({\"action\": \"stealth_mode\"})\n                        return json.dumps({\"message\": \"Stealth mode successful!\", \"status\": 200})\n                    except Exception as ex:\n                            logging.error(ex)\n                            logging.error(traceback.format_exc())\n                            return \"Stealth mode error\", 500\n                elif path == \"theme_refresh\":\n                    try:\n                        self.process_actions({\"action\": \"theme_refresh\"})\n                        return json.dumps({\"message\": \"Theme refresh successful!\", \"status\": 200}), 200\n                    except Exception as ex:\n                            logging.error(ex)\n                            logging.error(traceback.format_exc())\n                            return \"Theme refresh error\", 500\n                elif path == \"theme_select\":\n                    try:\n                        theme = request.args.get('theme')\n                        rotation = request.args.get('rotation')\n                        rot = int(rotation)\n                        self.theme_save_config(theme, rot)\n                        self.refresh = True\n                        return json.dumps({\"message\": \"theme selection successful!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"theme selection error\", 500\n                elif path == \"plugin\":\n                    try:\n                        # Retrieve 'name' and 'enable' parameters from the query string\n                        name = request.args.get('name')\n                        enable = request.args.get('enable', 'false').lower() == 'true'\n\n                        if not name:\n                            return json.dumps({\"message\": \"Plugin name is missing.\", \"status\": 400}), 400\n\n                        # Process the toggle action\n                        self.process_actions({\"action\": \"plugin\", \"name\": name, \"enable\": enable})\n\n                        # Respond with success message\n                        return json.dumps({\"message\": f\"Plugin '{name}' toggled {'enabled' if enable else 'disabled'} successfully!\", \"status\": 200})\n                    except Exception as ex:\n                        logging.error(f\"Error toggling plugin: {ex}\")\n                        logging.error(traceback.format_exc())\n                        return json.dumps({\"message\": \"Plugin toggle error\", \"status\": 500}), 500\n                elif path == \"btn_cmd\":\n                    try:\n                        screen = 1\n                        action = request.args.get('action')\n                        hardware = request.args.get('hardware')\n                        scr = request.args.get('screen')\n                        self.log(f\"screen: {screen}\")\n                        if scr is None:\n                            if self.dispHijack:\n                                screen = 2\n                        else:\n                            screen = scr\n                        self.log(f\"btn_cmd: {action}\")\n                        self.log(f\"hardware: {hardware}\")\n                        \n                        \n                        \n                        action_mapping = {\n                            'up': 'btn_up',\n                            'down': 'btn_down',\n                            'left': 'btn_left', \n                            'right': 'btn_right',\n                            'select': 'btn_select',\n                            'start': 'btn_start',\n                             'a': 'btn_a',\n                             'b': 'btn_b',\n                             'x': 'btn_x',\n                             'y': 'btn_y',\n                             'l1': 'btn_l1',\n                             'l2': 'btn_l2',\n                             'r1': 'btn_l1',\n                             'r2': 'btn_l2',\n                        }\n                        \n                        btn_action = action_mapping.get(action)\n                        self.log(f\"btn_cmd: {btn_action}\")\n                        if btn_action:\n                            self.button_controller({\"action\": btn_action}, screen=screen)\n                            #self.navigate_fancymenu({\"action\": btn_action})\n                            return json.dumps({\"message\": f\"{btn_action} successful!\", \"status\": 200})\n                        else:\n                            return \"Invalid navigation action\", 400\n                            \n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Navigation error\", 500\n                elif path == \"reset_css\":\n                    try:\n                        original_css_backup = os.path.join(self._pwny_root, 'ui/web/static/css/style.css.backup')\n                        with open(original_css_backup, 'w+') as f:\n                            f.write(CSS)\n                        return json.dumps({\"message\": \"CSS reset successful!\", \"status\": 200})\n                    except Exception as e:\n                        self.log(f\"Error: {e}\")\n                        return \"Error resetting CSS\", 500\n\n            elif request.method == \"POST\":\n                if path == \"version_compare\":\n                    is_newer = None\n                    local_version = None\n                    try:\n                        jreq = request.get_json() \n                        response = json.loads(json.dumps(jreq)) \n                        theme = response['theme']\n                        version = response['version']\n                        self.log(f'Download selection: theme {theme} version {version}')\n                        info_path = os.path.join(self._plug_root, \"themes\", theme, \"info.json\")\n                        if not os.path.exists(info_path):\n                            logging.error(f\"Theme {theme} not found locally.\")\n                        else:\n                            with open(info_path, 'r') as f:\n                                local_info = json.load(f)\n                                local_version = local_info.get('version')\n                            if local_version is None:\n                                self.log(f\"Local version not found for theme {theme}.\")\n                                local_version = 'Unknown'\n                            self.log(f\"Local theme version: {local_version}\")\n                            is_newer = version > local_version if local_version != 'Unknown' else False\n                        self.log(f'Is the online theme newer: {is_newer}')\n                        return json.dumps({\n                            'is_newer': is_newer,\n                            'local_version': local_version\n                        }), 200\n                    except Exception as ex:\n                        logging.error(f\"Error handling theme version: {ex}\")\n                        logging.error(traceback.format_exc())\n                        return json.dumps({'error': 'Theme version error'}), 500\n                if path == \"theme_download_select\":\n                    try:\n                        jreq = request.get_json()\n                        response = json.loads(json.dumps(jreq))\n                        theme = response['theme']\n                        self.log(f'Download selection: theme {theme}')\n                        self.theme_downloader(theme)\n                        return \"success\", 200\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return json.dumps({'error': f\"theme download error: {ex}\"}) , 500\n                elif path == \"save_config\":\n                    try:\n                        data = request.get_json()\n\n                        config = data.get('config')\n                        css = data.get('css')\n                        info = data.get('info')\n                        theme_cfg = {}\n                        theme_cfg['theme'] = config['theme']\n                        self.save_active_config(theme_cfg)\n                        if self._th_path:  \n                            css_src = os.path.join(self._th_path, 'style.css')\n                            info_src = os.path.join(self._th_path, 'info.json')\n\n                            if info != \"No custom Info\":\n                                if os.path.exists(info_src):\n                                    os.remove(info_src)\n                                with open(info_src, 'w') as info_file:\n                                    info_file.write(info)\n                                self.log(f\"Updated Info files at {self._th_path}\")\n\n                            if css != \"No custom CSS\":\n                                if os.path.exists(css_src):\n                                    os.remove(css_src)\n                                with open(css_src, 'w') as css_file:\n                                    css_file.write(css)\n                                self.log(f\"Updated CSS files at {self._th_path}\")\n                            \n                            self.log(f\"Updated CSS and Info files at {self._th_path}\")\n                        self.refresh = True\n                        return \"Configuration saved successfully\", 200\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Error saving configuration\", 500\n                elif path == \"create_theme\":\n                    try:\n                        data = request.get_json()\n                        theme_name = data['theme_name']\n                        use_resolution = data['use_resolution']\n                        use_orientation = data['use_orientation']\n                        is_created = self.theme_creator(theme_name, state=self._state, oriented=use_orientation, resolution=use_resolution)\n                        if is_created:\n                            return \"Theme created successfully\", 200\n                        else:\n                            return \"Theme with same name is existing\", 500\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Error creating theme\", 500\n                elif path == \"theme_copy\":\n                    try:\n                        data = request.get_json()\n                        theme = data['theme']\n                        new_name = data['new_name']\n                        themes_folder = os.path.join(self._plug_root, 'themes')\n                        src_path = os.path.join(themes_folder, theme)\n                        dst_path = os.path.join(themes_folder, new_name)\n                        \n                        if os.path.exists(dst_path):\n                            self.log(f\"Theme '{theme}' already exists. Skipping creation.\")\n                            return \"Theme with same name is existing\", 500\n\n                        if os.path.exists(src_path):\n                            copytree(src_path, dst_path)\n                            return \"Theme copied successfully\", 200\n                        else:\n                            return \"Source theme not found\", 404\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Error copying theme\", 500\n                elif path == \"theme_rename\":\n                    try:\n                        data = request.get_json()\n                        theme = data['theme']\n                        new_name = data['new_name']\n                        themes_folder = os.path.join(self._plug_root, 'themes')\n                        src_path = os.path.join(themes_folder, theme)\n                        dst_path = os.path.join(themes_folder, new_name)\n                        \n                        if os.path.exists(dst_path):\n                            self.log(f\"Theme '{theme}' already exists. Skipping creation.\")\n                            return \"Theme with same name is existing\", 500\n\n                        if os.path.exists(src_path):\n                            os.rename(src_path, dst_path)\n                            return \"Theme renamed successfully\", 200\n                        else:\n                            return \"Theme not found\", 404\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"Error renaming theme\", 500\n                elif path == \"theme_upload\":\n                    try:\n                        if 'zipFile' in request.files:\n                            file = request.files['zipFile']\n                            if file.filename == '':\n                                return 'No selected file', 400\n                            if file and allowed_file(file.filename):\n                                filename = file.filename\n                                themepath = os.path.join(self._plug_root, 'themes')\n                                filepath = os.path.join(themepath, filename)\n                                file.save(filepath)\n\n                                with tempfile.TemporaryDirectory() as temp_dir:\n                                    unzip_file(filepath, temp_dir)\n\n                                    folders_in_zip = [\n                                        name for name in os.listdir(temp_dir)\n                                        if os.path.isdir(os.path.join(temp_dir, name))\n                                    ]\n\n                                    existing_folders = []\n                                    for folder in folders_in_zip:\n                                        target_folder = os.path.join(themepath, folder)\n                                        if os.path.exists(target_folder):\n                                            existing_folders.append(folder)\n\n                                    if existing_folders:\n                                        return f'{existing_folders} folders were not copied because they already exist.', 400\n\n                                    for folder in folders_in_zip:\n                                        source = os.path.join(temp_dir, folder)\n                                        target = os.path.join(themepath, folder)\n                                        copytree(source, target)\n\n                                return 'Zip file uploaded and extracted successfully', 200\n                            else:\n                                return 'Invalid file type', 400\n                        else:\n                            return 'No file part in the request', 400\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return 'Theme upload error', 500\n                elif path == \"theme_delete\":\n                    try:\n                        jreq = request.get_json()\n                        response = json.loads(json.dumps(jreq))\n                        theme = response['theme']\n                        if theme in ['', self._theme_name]:\n                            self.log('theme can\\'t be deleted')\n                            return \"theme can't be deleted\", 500\n                        else:\n                            themepath = os.path.join(self._plug_root, 'themes')\n                            filepath = os.path.join(themepath, theme)\n                            self.log(f'Delete theme at {filepath}')\n                            os.system(f'rm -r {filepath}')\n                            return \"success\"\n                    except Exception as ex:\n                        logging.error(ex)\n                        logging.error(traceback.format_exc())\n                        return \"theme selection error\", 500\n                elif path == \"theme_info\":\n                    jreq = request.get_json()\n                    response = json.loads(json.dumps(jreq))\n                    theme = response['theme']\n                    descpath = os.path.join(self._plug_root, 'themes', theme, 'info.json')\n                    info = {\n                        \"author\": \"Unknown\",\n                        \"version\": \"Unknown\",\n                        \"display\": \"Unknown\",\n                        \"plugins\": \"Unknown\",\n                        \"notes\": \"Unknown\"\n                    }\n                    if theme in ['Default', None]:\n                        descpath = None\n                        info = {\n                                \"author\": \"<a href='https://github.com/V0r-T3x'>V0rT3x</a>\",\n                                \"version\": \"1.0.0\",\n                                \"display\": \"All\",\n                                \"plugins\": \"all\",\n                                \"notes\": \"Default theme\"\n                            }\n\n                    try:\n                        if descpath is not None:\n                            if descpath and os.path.exists(descpath):\n                                with open(descpath, 'r') as json_file:\n                                    info = json.load(json_file)\n                        theme_info = info\n                        return json.dumps(theme_info, default=serializer), 200\n\n                    except Exception as ex:\n                        logging.error(f\"Error in theme info: {str(ex)}\")\n                        logging.error(traceback.format_exc())\n                        return \"theme selection error\", 500\n\n        except Exception as e:\n            self.log(f\"Error in webhook: {str(e)}\")\n            self.log(traceback.format_exc())\n            return None"
  },
  {
    "path": "README.md",
    "content": "# <p align=\"center\">🪄FANCYGOTCHI 2.0🖌️</p>\r\n\r\n<div align=\"center\">\r\n  <h1> 🎉 🚀Join us on <a href=\"https://discord.gg/78dGjukKU9\">discord</a>🚀 🎉 </h1>\r\n</div>\r\n\r\n<div align=\"center\">\r\n  <h2> 🎵 Listen to the AI song: <a href=\"https://www.youtube.com/watch?v=vYnJTlbz79U\" target=\"_blank\">Symbiote Skin (I 💜 Fancygotchi) by Kari] </a> 🎵 </h2>\r\n</div>\r\n\r\n<p align=\"center\">\r\n<img src='https://github.com/V0r-T3x/fancygotchi/blob/main/.assets/fancygotchi2.0.png' width='500px'></img>\r\n</p>\r\n\r\n*Fancygotchi is the ultimate theme manager for pwnagotchi*\r\n\r\n> \"Are you ready to be refaced!?\"  \r\n> - *Fancygotchi, 2024.*\r\n\r\n## Disclaimer\r\n> From **V0r-T3x** (inspired by [**roodriiigooo**](https://github.com/roodriiigooo)): The content here is free for use, but it doesn't mean you can use it however you want. No author or contributor assumes responsibility for the misuse of this device, project, or any component herein. The project and modifications were **developed solely for educational purposes**.\r\n> Any files, plugins or modifications of this project or original project found here should **not be sold**. In the case of use in open projects, videos or any form of dissemination, please remember to give credit to the repository ♥  \r\n\r\n*I'm in active development, if you want encourage me, become a [Patreon](https://patreon.com/v0rt3x_workshop) or send me a voluntary contribution with [Paypal](https://www.paypal.com/paypalme/v0r73x?country.x=CA&locale.x=en_US)*  \r\n\r\n# ** The raspberry pi zero W need a fix to run, you need to use [`/pwnagotchi/plugins/__init__.py`](https://github.com/Sniffleupagus/pwnagotchi-snflpgs/blob/snflpgs/pwnagotchi/plugins/__init__.py) **\r\n# ** The rpi4 need some additional steps to access all the features, this will be documented soon **\r\n\r\n# :books: The documentation is available in the [FANCYGOTCHI WIKI](https://github.com/V0r-T3x/fancygotchi/wiki)\r\n# :art: The theme v2.0 are available [FANCYGOTCHI 2.0 THEMES](https://github.com/V0r-T3x/Fancygotchi_themes/tree/main/fancygotchi_2.0)\r\n\r\n\r\n"
  },
  {
    "path": "config.toml",
    "content": "main.plugins.Fancygotchi.enabled = true #<-- Fancygotchi will generate a default config \nmain.plugins.Fancygotchi.rotation = 0 # 0 or 180 is horizontal mode, 90 or 270 is vertival mode\nmain.plugins.Fancygotchi.theme = \"\" #<-- if empty the default theme will be loaded\nmain.plugins.Fancygotchi.fancyserver = false # Fancyserver is used for FancyMenu and for additional control. It is disabled by default.\n\n#ui.fps = 1 #<-- need to have an higher value than 0.0\n\n#ui.display.enabled = true\n#ui.display.rotation = 0 #<-- This value need to stay 0\n#ui.display.type = \"displayhatmini\" #<-- select your display\n\n#fs.memory.mounts.data.enabled = false #<-- need to be false to avoid the errno 30 and the tmp file become read only and the pwnagotchi can't save his data\n#fs.memory.mounts.data.mount = \"/var/tmp/pwnagotchi\"\n#fs.memory.mounts.data.size = \"50M\"\n#fs.memory.mounts.data.sync = 3600\n#fs.memory.mounts.data.zram = false #<-- need to be false to avoid the errno 30 and the tmp file become read only and the pwnagotchi can't save his data\n#fs.memory.mounts.data.rsync = true\n"
  },
  {
    "path": "fancyshow.py",
    "content": "import logging\nimport time\nimport pwnagotchi.plugins as plugins\n\n# This plugin is designed to demonstrate how to use the Fancygotchi's partial update feature (ui._update).\n# It modifies the 'name' widget's position and color, verifies the changes, and reverts them upon unload.\n\n\nclass Fancyshow(plugins.Plugin):\n    __author__ = 'V0r-T3x'\n    __version__ = '1.0.0'\n    __license__ = 'GPL3'\n    __description__ = 'An example plugin to show how to use Fancygotchi\\'s ui._update feature.'\n\n    def __init__(self):\n        logging.debug(\"fancyshow plugin created\")\n        # A dictionary to store options for the plugin (not used in this example but good practice).\n        self.options = dict()\n        # This will store the original properties of the widget we are modifying.\n        self.original_widget_options = {}\n        # A flag to ensure we only set our desired position once.\n        self.position_set = False\n        # Flags to control the plugin's state, especially during unload.\n        self.unloaded = False\n        self.reverting = False\n        # The new widget properties we want to apply.\n        self.widget_options = {\n            'position': [\"center\", \"center\"],\n            'color': [\"lime\", \"red\"]\n        }\n        # A counter to track if the UI state is consistently different from what we expect.\n        self.discrepancy_counter = 0\n\n    # called when the plugin is loaded\n    def on_loaded(self):\n        logging.info(\"fancyshow plugin loaded.\")\n        # Reset the unloaded flag when the plugin is loaded/reloaded.\n        self.unloaded = False\n\n    # called when the plugin is unloaded\n    def on_unload(self, ui):\n        logging.info(\"fancyshow plugin unloading...\")\n        self.reverting = True\n        # Check if we have stored original options to revert to.\n        if self.original_widget_options and hasattr(ui, '_update'):\n            # Use Fancygotchi's partial update mechanism to revert the 'name' widget's properties.\n            ui._update.update({\n                'update': True,\n                'partial': True,\n                'dict_part': {'widget': {'name': self.original_widget_options}}\n            })\n            logging.info(f\"fancyshow: attempting to revert 'name' options to {self.original_widget_options}\")\n\n            # Wait for the UI to confirm the change, with a timeout.\n            timeout = 10  # seconds\n            start_time = time.time()\n            reverted = False\n            while time.time() - start_time < timeout:\n                # The `ui.fancy._state` attribute is provided by Fancygotchi and contains the current theme state.\n                # We check this to verify that our reversion request has been processed.\n                if hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state and self.original_widget_options:\n                    name_state = ui.fancy._state['name']\n                    all_reverted = all(\n                        # Normalize lists to tuples for comparison, handles nested lists if any.\n                        (tuple(current) if isinstance(current, list) else current) ==\n                        (tuple(expected_val) if isinstance(expected_val, list) else expected_val)\n                        for prop, expected_val in self.original_widget_options.items()\n                        for current in [name_state.get(prop)])\n                    if all_reverted:\n                        logging.info(\"fancyshow: successfully reverted 'name' position.\")\n                        reverted = True\n                        break\n                time.sleep(0.5)  # Check every half-second\n\n            if not reverted:\n                logging.warning(f\"fancyshow: could not verify 'name' position was reverted within {timeout} seconds.\")\n\n        # Final cleanup of state variables for a clean unload.\n        self.unloaded = True\n        self.original_widget_options = {}\n        self.position_set = False\n\n    # This is called by Pwnagotchi when the UI is being set up.\n    # called to set up the ui elements\n    def on_ui_setup(self, ui):\n        self._get_initial_options(ui)\n\n    # called when the ui is updated\n    def on_ui_update(self, ui):\n        if self.unloaded or self.reverting:\n            # Do nothing if the plugin is in the process of unloading.\n            return\n\n        # Get original position if we haven't already\n        if not self.original_widget_options:\n            self._get_initial_options(ui)\n            # If we still don't have it, we can't do anything yet.\n            if not self.original_widget_options:\n                return\n\n        # This block runs only once to apply the new widget options.\n        if not self.position_set and hasattr(ui, '_update') and not ui._update.get('update', False):\n            # Request a partial theme update to change the 'name' widget.\n            ui._update.update({\n                'update': True,\n                'partial': True,\n                'dict_part': {'widget': {'name': self.widget_options}}\n            })\n            self.position_set = True\n            logging.info(f\"fancyshow: setting 'name' options to {self.widget_options}\")\n\n        # This block runs on subsequent updates to verify that our changes have been applied and have persisted.\n        # The UI state can be changed by other plugins or theme updates, so this check is important.\n        # After setting, we can verify its position on subsequent updates\n        if self.position_set and hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state:\n            name_state = ui.fancy._state['name']\n            for prop, expected_value in self.widget_options.items():\n                current_value = name_state.get(prop)\n                # Normalize lists to tuples for comparison\n                if isinstance(expected_value, list):\n                    expected_value = tuple(expected_value)\n                if isinstance(current_value, list):\n                    current_value = tuple(current_value)\n                \n                if current_value != expected_value:\n                    # If the current state doesn't match our expected state, increment a counter.\n                    # This might happen temporarily if another plugin is also updating the UI.\n                    self.discrepancy_counter += 1\n                    logging.warning(f\"fancyshow: 'name' property '{prop}' is {current_value}, not {expected_value} as expected. Discrepancy count: {self.discrepancy_counter}\")\n                    if self.discrepancy_counter >= 15:\n                        logging.warning(\"fancyshow: Discrepancy threshold reached. Investigating cause...\")\n                        \n                        # Check if a pending partial update is the cause of the discrepancy\n                        if hasattr(ui, '_update') and ui._update.get('update') and ui._update.get('partial'):\n                            pending_changes = ui._update.get('dict_part', {}).get('widget', {}).get('name', {})\n                            if pending_changes:\n                                # Compare pending changes to our saved original options\n                                if any(pending_changes.get(p) != self.original_widget_options.get(p) for p in pending_changes):\n                                    logging.info(\"fancyshow: Detected a pending theme change. Updating baseline and re-applying options.\")\n                                    # The pending update is the new baseline\n                                    self.original_widget_options.update(pending_changes)\n                        else:\n                            # No pending update, so re-check the current state from the UI\n                            logging.info(\"fancyshow: No pending update detected. Re-evaluating original state from UI.\")\n                            self._get_initial_options(ui)\n\n                        # Reset the flag to force re-application of widget_options\n                        self.position_set = False\n                        # Reset the counter\n                        self.discrepancy_counter = 0\n                        # Break the loop to allow re-application in the next on_ui_update cycle\n                        break\n\n    # A helper function to safely get the initial properties of the 'name' widget.\n    def _get_initial_options(self, ui):\n        # `ui.fancy._state` is a special attribute injected by Fancygotchi that holds the current theme's state.\n        if hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state:\n            name_state = ui.fancy._state['name']\n            new_original_options = {}\n            changed = False\n            # We only care about the properties we intend to modify.\n            for prop in self.widget_options.keys():\n                if prop in name_state:\n                    current_original_value = name_state.get(prop)\n                    new_original_options[prop] = current_original_value\n                    # Check if the baseline has changed since we last checked\n                    if self.original_widget_options and self.original_widget_options.get(prop) != current_original_value:\n                        changed = True\n                else:\n                    logging.warning(f\"fancyshow: could not find original value for property '{prop}'.\")\n            \n            # If this is the first time, store the fetched options.\n            if not self.original_widget_options:\n                self.original_widget_options = new_original_options\n                logging.info(f\"fancyshow: Stored initial 'name' options: {self.original_widget_options}\")\n            elif changed:\n                # Check for false positive: if the \"new\" origin is what we're trying to set,\n                # it's likely our own change being read back. Don't update the baseline.\n                is_false_positive = all(\n                    tuple(new_original_options.get(prop)) == tuple(val) if isinstance(val, list) else new_original_options.get(prop) == val\n                    for prop, val in self.widget_options.items()\n                )\n                if is_false_positive:\n                    logging.warning(\"fancyshow: Detected a potential false positive. The new baseline matches widget_options. Not updating original options.\")\n                else:\n                    logging.warning(f\"fancyshow: Original 'name' options changed from {self.original_widget_options} to {new_original_options}. Updating baseline.\")\n                    self.original_widget_options = new_original_options\n            else:\n                logging.debug(\"fancyshow: Re-checked original 'name' options. No changes to baseline.\")\n"
  }
]