Full Code of V0r-T3x/Fancygotchi for AI

main bf5f4fc61c3d cached
7 files
298.1 KB
67.8k tokens
116 symbols
1 requests
Download .txt
Showing preview only (314K chars total). Download the full file or copy to clipboard to get everything.
Repository: V0r-T3x/Fancygotchi
Branch: main
Commit: bf5f4fc61c3d
Files: 7
Total size: 298.1 KB

Directory structure:
gitextract_z1nv9f52/

├── .github/
│   ├── FUNDING.yml
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── Fancygotchi.py
├── README.md
├── config.toml
└── fancyshow.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: V0rt3x_workshop # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: v0r_t3x # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: ''
assignees: ''

---

**Pwnagotchi Version**: ``
**Hardware (SBC)**: ``
**Screen Type**: ``
**Other Hardware Used**: ``
**Pwnagotchi Fork (if applicable)**: ``

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.

**Logs**
- Run the diagnostic script and attach the log archive here (if relevant).


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: Fancygotchi.py
================================================
# adding api or ui attribute to know the actual theme config and or fancy state

import argparse
import asyncio
import copy
import importlib
import glob
import gettext
import importlib.util
import json
import logging
import math
import numpy as np
import os
import random
import re
import requests
import secrets
import shutil
import struct
import subprocess
import sys
import tempfile
import threading
import time
import toml
import traceback
import zipfile

from io import BytesIO
from multiprocessing.connection import Client, Listener
from os import system
from shutil import copy2, copyfile, copytree
from textwrap import TextWrapper
from toml import dump, load
from PIL import Image, ImageChops, ImageDraw, ImageFont, ImageOps, ImageSequence
from flask import abort, jsonify, make_response, render_template_string, send_file, session

import pwnagotchi
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.faces as faces
import pwnagotchi.ui.fonts as fonts
from pwnagotchi import utils
from pwnagotchi.plugins import toggle_plugin
from pwnagotchi.ui import display
from pwnagotchi.ui.hw import display_for
from pwnagotchi.utils import load_config, merge_config, save_config

V0RT3X_REPO = "https://github.com/V0r-T3x"
FANCY_REPO = os.path.join(V0RT3X_REPO, "Fancygotchi")
THEMES_REPO = "https://api.github.com/repos/V0r-T3x/Fancygotchi_themes/contents/fancygotchi_2.0/themes"


LOGO = """░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒░░░░░░▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓████▓▓▓▓▓▓▓▓▓████████▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓███████▓▓▓▓▓▓▓▓██████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓█████▓▓▓▓▓▓▓▓▓▓▓██████████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓█▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓███████████▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█▓▓▓█▓▒▒▒▒▓▓▓▓▓▓▓▓▓█████████████▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░▒▓▓▓█▓▓▓█▓▓██████████████████████████████▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░███████████████████████████████████████████████████▓░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░████████████████████████████████████████████████████░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░▒████████▓▓▓▓▓▓▓▓██████████████████████████████████▒░░░░▒▒▒▒░░░░░░░░░░
░░░░░░░░░▓▓▒░░░░░░░░░░▓█████▓▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████▓░░░░░░▓▓▓▓▓▓▓▓▓░░░░░
░░░░░░░░▒▒▒▓▒░░░░░░░░░░░▒▓██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓██▓▒░░░░░░░░░█▓▓▓▓▓█▓░░░░░░
░░░░░░░░▓░░▒▒▓▒░░░░░░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓░░░░░░░░░░░▒▒▓▓▓▓▓█▒░░░░░░
░░░░░░░▓▒▒▒▒▒▓▓▒░░░░░░░░░░░▓▒▒▓████▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█████▓▓▓▓░░░░░░░░░░░▒▒▒▒▒▒▓▓░░░░░░░
░░░░░░░▒▒░░░░▒▒▓░░░░░░░░░░░▓▒▒▒██▓▓████▓▒▒▒▒▒▒▒▒▒▒▓▓████▓███▓▓▓▒░░░░░░░░░░▒▓▓▓▓▓▒▓▒░░░░░░░
░░░░░░░░▓░░░░░▒▓░░░░░░░░░░░░▓▒▒▒███████▓▒▒▒▒▒▒▒▒▒▒▒▓███████▓▓▓▓░░░░░░░░░░░░░▓▓██▓▓░░░░░░░░
░░░░░░░▒▓▒░░░░▓▓▒▒▒░░░░░░░░░▒▓▒▒▒▓███▓▒▒▓▓▓▓▒▒▒▓▓▓▒▒▒▓████▓▓▓▓░░░░░░░░░░░▒▒▓▓▓█░░░░░░░░░░░
░░░░░░░▒▓▓▒▓▒▒▓▓▓█▓▓░░░░░░░░░░▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▓▓▓▓░░░░░░░░░░░▓▒▒█▓▓▓░░░░░░░░░░░
░░░░░░░░▒█▒▓▓▓▓▓███▓▒░░░░░░░░▒▓█▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓██▒░░░░░░░░░░░█▓▓█▓█▒▒▒░░░░░░░░░
░░░░░░░░░▓░▓▓▓▒▒▓██▓▓▓▒░░░░▒▓███▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓████▓▒░░░░░░░░▓▓██████▓▒▓░░░░░░░░
░░░░░░░░░▒▓▒▒▒▓▓████▓▓▓▓▒▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓███▓▒▒░▒▒▓▒▒▓██████▓▒▓░░░░░░░░
░░░░░░░░░░░░░▒████▓██▓▓▓▓▓▓▓▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▓██████▓▒▓▒░░░░░░░░
░░░░░░░░░░░░░░▒████▓▒▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓█████▓▒▒░░░░░░░░░░
░░░░░░░░░▒██░░░▓█████▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓█▓▒▒▒▒▒▒▓█▓▓▒▒▒▒▒▒▓█▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓███▓█▓▒▒░░░░░░░░░░░
░░░░░░░░▒██░░░░▒▒████████▓▓▓▓▓▓██████▓▒▒▒▒▒▓█████▓▒▒▒▒▒████████▓▓▓▓▓█████░▒██▓██▓░░░░░░░░░
░░░░░░░░▓██░░░▒░░▒█████████▓▓████████▓▒▒▒▒▓███████▓▒▒▒▒████████████████▓░░░▒▒░▒██▓░░░░░░░░
░░░░░░░░▒███▒░░░▒████▒░██▓███▓███████▒▒▒▒▓█████████▓▒▒▒▒████████▓██▓██▓░░░░░░░▒███░░░░░░░░
░░░░░░░░░▒███████████▒▒█▓▓██▓▓▓█████▒▒▒▒▒███████████▓▒▒▒▒██████▓▓█████▓▒░░░░░░▓██▓░░░░░░░░
░░░░░░░░░░░▒▓▓██████▓▓███▓███▓▒▒▓▓▒▒▒▒▒▓██████████████▒▒▒▒▒▓▓▒▒▒██████▓▓▓▒▒▒▓▓███▒░░░░░░░░
░░░░░░░░░░░░░▓███████████▓▓████▓▓▒▒▒▓▓███████▓▒▓██▓█████▓▒▒▒▒▒▓████████████████▓░░░░░░░░░░
░░░░░░░░░░░░░░▓███████████▓███████████████▒░░░░░░░▓▓██████████████▓█████████▓▒░░░░░░░░░░░░
░░░░░░░░░░░░░░░▒▓▓██████▓░░▒█████████████▒░░░░░░░░░▓█▓███████████▒░░▓▓█▓▓▓▓▒░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░▒░░░░░░░▒▓██▓██▓███▓▒░░░░░░░░░░░░▓██▓█████▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░"""

INDEX = """
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
    Fancygotchi
{% endblock %}
{% block meta %}
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=0" />
{% endblock %}
{% block styles %}
{{ super() }}
<style>
    body {
        position: relative;
        min-height: 100vh;
    }
    #wrap {
        width: 100%;
        float:left;
        padding: 10px;
        padding-bottom: 50px;
        align-items: center;
        justify-content: center;
        flex-grow: 1;
        display: flex;
        border-bottom: 1px solid black;
        flex-direction: column; /* Display tabs menu and content vertically */}
    #tabs {
        border-bottom: 1px solid black;
        text-align: center;}
    .theme {
        width: 100%;
        margin-bottom: 20px;}
    .theme-columns {
        display: flex;}
    .select,
    .theme-description {
        flex: 1;
        margin-right: 20px;}
    #uploader {
        margin-top: 20px;}
    #tabs{
        width: 100%}
    #config_content {
        text-align: left;}
    label {
        text-align: center;}
    .ui-image {
        width: 100%;
        max-width: 600px; 
        left: 50%;
        transform: translateX(-50%);
        position: relative;
        background-color: black;
    }
    .config-box {
        max-width: 100%;
        width: 600px;
        max-height: 300px;
        resize: both; /* allow resizing in both directions */
        overflow: auto;
        padding: 10px;
        border: 1px solid #ccc;}
    #fancygotchi {
        font-size: 10px;
        text-align: center;
        white-space: pre; /* Preserve the spaces in ASCII art */
        font-family: monospace;
    }
    #sticky-button {
        max-width: 150px;
        position: fixed;
        bottom: 15px; /* Distance from the bottom of the screen */
        left: 50%; /* Center the button horizontally */
        transform: translateX(-50%); /* Adjust the centering */
        cursor: pointer;
        z-index: 1000; /* Ensure it stays above other elements */
    }
    .preserve-line-breaks {
        white-space: pre-wrap;
    }
    .glitch-line {
        display: inline-block;
        position: relative;
        animation: glitch 0.3s ease-in-out forwards; /* Slower animation duration */
    }
    /* Style the button */
    .scroll-to-top-btn {
        max-width: 100px;
        position: fixed;
        bottom: 00px;
        right: 40px;
        z-index: 100; /* Ensure it's on top of other elements */
        cursor: pointer;
        display: none; /* Initially hidden */
        font-size: 24px; /* Make the arrow bigger */
    }
    #theNet{
        display: none;
        position: absolute;
        bottom: 0px;
        font-size: 9px;
        right: 25px;
        padding:10px;
        cursor: context-menu;
        -webkit-touch-callout: none; /* iOS Safari */
            -webkit-user-select: none; /* Safari */
                -khtml-user-select: none; /* Konqueror HTML */
                    -moz-user-select: none; /* Old versions of Firefox */
                        -ms-user-select: none; /* Internet Explorer/Edge */
                            user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
    }

    /* Show button when scrolling */
    .scroll-to-top-btn.show {
        display: block;
    }
    #footer {
        backkground-color: black;
        position: fixed;
        bottom: 0;
        width: 400px;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 1000;
        text-align: center;
    }
    #logo {
        #left: 50%;
        #transform: translate(-50%, -50%);
        margin-right: 40px;
        margin-left: 40px;
    }
    .dev {
        #display: none;
    }

    @keyframes glitch {
        0% {
            transform: translateX(0);
        }
        20% {
            transform: translateX(-2px); /* Smaller shift */
        }
        40% {
            transform: translateX(2px); /* Smaller shift */
        }
        60% {
            transform: translateX(-1px); /* Smaller shift */
        }
        80% {
            transform: translateX(1px); /* Smaller shift */
        }
        100% {
            transform: translateX(0);
        }
    }

    /* Main container with 3 sections */
    .container {
        display: flex; /* Horizontal alignment */
        align-items: flex-start; /* Align items to the top */
        width: 100%;
        gap: 0px; /* No gap between containers */
        max-height: 100%;
    }

    /* Left section (ensures minimal size) */
    .left-container {
        display: block; /* Prevent flex stretching */
        width: 33.33%;
        max-width: 100%;
        min-height: 100%; /* Minimum height is set to 100% of the container */
        max-height: 100%; /* Maximum height is 100% of the container */
    }

    /* Grid layout inside the left-container */
    .left {
        display: grid;
        grid-template-columns: repeat(6, minmax(20px, 1fr)); /* Equal column width */
        grid-template-rows: repeat(6, minmax(35px, auto)); /* Dynamic rows with a minimum height */
        gap: 0px; /* No spacing between grid items */
        width: 100%; /* Fit the container width */
        height: auto; /* Adjusts automatically based on content */
        min-height: 100%; /* Ensures it doesn’t collapse */
    }

    /* Flip switch buttons */
    .left button {
        display: block; /* Prevent inline layout issues */
        width: 100%; /* Fit the parent width */
        max-width: 100%; /* Prevent overflow */
        padding: 0; /* Remove extra padding */
        margin: 0; /* Remove extra margin */
    }

    .left-top {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        width: 100%; /* Fit the parent width */
        height: auto; /* Adjusts automatically based on content */
        min-height: 100%; /* Ensures it doesn’t collapse */
        gap: 30px;
        align-items: center;
        padding-top: 0px;
        margin-top: 0px;
        justify-content: center;
    }

    .left-top ui.flipswitch {
        justify-self: center;
        align-self: center;
        margin: auto;
        width:100%;
        position: relative;
        display: inline-block;
    }

    .left-top div{
        max-width: 100%;
        width: 100%;
    }

    /* Center element truly centered */
    .center-container {
        display: flex;
        justify-content: center;
        align-items: center; /* Ensures vertical centering */
        width: 33.33%;
        min-width: 400px;
        min-height: 100%; /* Allow content to expand with the container */
        max-height: 100%; /* Ensure no stretching beyond the container */
    }

    .center img {
        max-width: 100%;
        height: auto;
    }

    /* Right section centered within its area */
    .right-container {
        display: flex;
        justify-content: center;
        align-items: center; /* Ensures vertical centering */
        width: 33.33%;
        min-width: 400px;
        min-height: 100%; /* Allow content to expand with the container */
        max-height: 100%; /* Ensure no stretching beyond the container */
    }

    .right img {
        max-width: 100%;
        height: auto;
    }

    /* Responsive stacking for small screens */
    @media (max-width: 800px) {
        .container {
            flex-direction: column;
            align-items: center;
        }

        .left-container, .center-container, .right-container {
            width: 100%; /* Full width on smaller screens */
            margin-bottom: 10px;
        }
    }

    .btn_grey {
        background-color: #f2f2f2;
    }
    .btn_red {
        background-color: #ff0000;
    }
    .btn_green {
        background-color: #00ff00;
    }
    .ui-btn .btn_blue {
        background-color: #0000ff;
        color: #0000ff;
    }
    .btn_yellow {
        background-color: #ffff00;
    }
    .btn_black {
        background-color: #000000;
    }
    .btn_white {
        background-color: #ffffff;
    }

    /* Style individual arrow buttons */
    .arrow-button {
        font-weight: bold;
        font-size: 24px; /* Makes the arrow icon larger */
    }
    #download_window {
        display: none;
    }
    #loading-spinner{
    
    }
</style>
{% endblock %}
{% block content %}
    <div id="editor">
        
        <div id="main" data-role="navbar">
            <ul>
                <li>
                    <form class="action" method="post" action="/shutdown" onsubmit="return confirm('this will halt the unit, continue?');">
                        <input type="submit" class="button ui-btn ui-corner-all" value="Shutdown"/>
                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
                    </form>
                </li>
                <li>
                    <form class="action" method="post" action="/reboot" onsubmit="return confirm('this will reboot the unit, continue?');">
                        <input type="submit" class="button ui-btn ui-corner-all" value="Reboot"/>
                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
                    </form>
                </li>

                <li>
                    <form class="action" method="post" action="/restart" onsubmit="return confirm('This will restart the service in Manu mode, continue?');">
                        <input type="submit" class="button ui-btn ui-corner-all" value="Restart in Manu mode"/>
                        <input type="hidden" name="mode" value="MANU"/>
                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
                    </form>
                </li>
                <li>
                    <form class="action" method="post" action="/restart" onsubmit="return confirm('This will restart the service in Auto mode, continue?');">
                        <input type="submit" class="button ui-btn ui-corner-all" value="Restart in Auto mode"/>
                        <input type="hidden" name="mode" value="AUTO"/>
                        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
                    </form>
                </li>
            </ul>
        </div>
        <div id="display" data-role="navbar" class="dev">
            <ul>
                <li>
                    <button id="display_hijack" onclick="display_hijack()">Second hardware display</button>
                </li>
                <li>
                    <button id="display_pwny" onclick="display_pwny()">Pwnagotchi hardware display</button>
                </li>
            </ul>
            <ul>
                <li>
                    <button id="display_next" onclick="display_next()">Next second screen mode</button>
                </li>
                <li>
                    <button id="display_previous" onclick="display_previous()">Previous second screen mode</button>
                </li>
                <li>
                    <button id="screen_saver_next" onclick="screen_saver_next()">Next screen saver</button>
                </li>
                <li>
                    <button id="screen_saver_previous" onclick="screen_saver_previous()">Previous screen saver</button>
                </li>
            </ul>
        </div>
        <div class="container">
            <!-- Left Div with 6x5 grid -->
            <div class="left-container">
                <div class="left-top">
                    <div>
                        <label for="screen">Screen</label>
                        <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">
                    </div>
                    <div><button id="stealth" onclick="stealth()">Stealth</button></div>
                    <div>
                        <label for="keyboard">Keyboard</label>
                        <input type="checkbox"data-role="flipswitch" name="Enabled" id="keyboard" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch">
                    </div>
                    
                </div>
                <div class="left">
                    <div><button style="background-color: #a3a5a4; color: #222;" id="l2" class="btn_grey" onclick="navigate('l2')">L2</button></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div><button style="background-color: #a3a5a4; color: #222;" id="r2" onclick="navigate('r2')">R2</button></div>
                    
                    <div><button style="background-color: #a3a5a4; color: #222;" id="l1" onclick="navigate('l1')">L1</button></div>
                    <div><button style="background-color: #515151; color: #222;" id="up" onclick="navigate('up')" class="arrow-button">↑</button></div>
                    <div></div>
                    <div></div>
                    <div><button style="background-color: #0749b4; color: #000077;" id="x" onclick="navigate('x')" class="arrow-button">X</button></div>
                    <div><button style="background-color: #a3a5a4; color: #222;" id="r1" onclick="navigate('r1')">R1</button></div>
                    
                    <div><button style="background-color: #515151; color: #222;" id="left" onclick="navigate('left')" class="arrow-button">←</button></div>
                    <div><button style="background-color: #515151; color: #222;"></div>
                    <div><button style="background-color: #515151; color: #222;" id="right" onclick="navigate('right')" class="arrow-button">→</button></div>
                    <div><button style="background-color: #008d45; color: #007700;" id="y" onclick="navigate('y')" class="arrow-button">Y</button></div>
                    <div></div>
                    <div><button style="background-color: #eb1a1d; color: #770000;" id="a" onclick="navigate('a')" class="arrow-button">A</button></div>

                    <div></div>
                    <div><button style="background-color: #515151; color: #222;" id="down" onclick="navigate('down')" class="arrow-button">↓</button></div>
                    <div><button style="background-color: #4e5955; color: #000;" id="select" onclick="navigate('select')">Select</button></div>
                    <div><button style="background-color: #4e5955; color: #000;" id="start" class="btn_grey" onclick="navigate('start')">Start</button></div>
                    <div><button style="background-color: #fece15; color: #777700;" id="b" onclick="navigate('b')" class="arrow-button">B</button></div>
                    <div></div>

                    <div></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div></div>
                </div>
            </div>
            

            <!-- Center Image -->
            <div class="center-container">
                <div class="center">
                    <img class="ui-image pixelated" src="/ui" id="ui" style="width: 400px" />
                </div>
            </div>

            <!-- Right Image -->
            <div class="right-container">
                <div class="right">
                    <img class="ui-image pixelated" src="/plugins/Fancygotchi/ui2" id="ui2" style="width: 400px" />
                </div>
            </div>
        </div>

        <div id="wrap" data-role="tabs">
            <div id="tabs" data-role="navbar">
                <ul>
                    <li class="ui-btn-active"><a href="#theme" data-theme="a" data-ajax="false">Theme manager</a></li>
                    <li class=""><a href="#theme_downloader" data-theme="a" data-ajax="false">Theme downloader</a></li>
                    <li class=""><a href="#config" data-theme="a" data-ajax="false">Configuration</a></li>
                    <li class=""><a href="#theme_editor" data-theme="a" data-ajax="false">Theme editor</a></li>
                </ul>
            </div>
            <div id="theme" class="ui-content theme">
                <div id="theme-columns" class="row theme-columns">
                    <div id="select" class="column select">
                        <label for="theme-selector">Select a theme:</label>
                        <select id="theme-selector">
                            <option value="Default"{% if default_theme == '' %}selected{% endif %}>Default</option>
                            {% for theme in themes %}
                            <option value="{{ theme }}"{% if default_theme == theme %}selected{% endif %}>{{ theme }}</option>
                            {% endfor %}
                        </select>
                        <br>
                        <label for="orientation-selector">Select an orientation:</label>
                        <select id="orientation-selector">
                            <option value=0{% if rotation == 0 %} selected{% endif %}>0</option>
                            <option value=90{% if rotation == 90 %} selected{% endif %}>90</option>
                            <option value=180{% if rotation == 180 %} selected{% endif %}>180</option>
                            <option value=270{% if rotation == 270 %} selected{% endif %}>270</option>
                        </select>
                        <button id="select-theme-button" onclick="theme_select()">Select Theme</button>
                        <button id="copy-theme-button" onclick="copyTheme()">Copy Theme</button>
                        <button id="rename-theme-button" onclick="renameTheme()">Rename Theme</button>

                        <button id="select-theme-button" onclick="theme_delete()">Delete Theme</button>
                        <button id="export-theme-button" onclick="theme_export()">Export Theme</button>
                        <div id="uploader" class="ui-content">
                            <form id="uploadForm" enctype="multipart/form-data">
                                <input type="file" name="zipFile" id="zipFile">
                                <input type="submit" value="Upload Theme Zip" onclick="theme_upload(event)">
                            </form>
                            <div id="message"></div>
                        </div>
                        <div id="create-theme">
                            <h3>Create New Theme</h3>
                            <input type="text" id="new-theme-name" placeholder="Enter new theme name">
                            <label><input type="checkbox" id="use-resolution"> Use Resolution System</label>
                            <label><input type="checkbox" id="use-orientation"> Use Orientation System</label>
                            <button id="create-theme-button" onclick="createNewTheme()">Create Theme</button>
                        </div>
                    </div>
                    <div id="theme-description" class="column theme-description">
                        <h3>Theme Description</h3>
                        <div id="theme-description-content"></div>
                        <img id="screenshot" src="/img/screenshot.png" onerror="this.onerror=null; this.src='/screenshots/screenshot.png';"></img>
                    </div>
                </div>
            </div>
            <div id="theme_downloader" class="ui-content theme">
                <div id="download_list_refresh">
                    <p align="center">
                        <button id="select-theme-downloader-btn" onclick="loadThemeRepo()">Load theme list</button>
                    </p>
                </div>
                <div id="loading-spinner" style="display:none;"><p align="center">Loading...</p></div>
                <div id="download_window" style="display:none;">
                    <div id="theme-downloader-columns" class="row theme-columns">
                        <div id="downloader-select" class="column select">
                            <label for="theme-downloader-selector">Select a theme:</label>
                            <select id="theme-downloader-selector">
                                <!-- Themes will be dynamically populated here -->
                            </select>
                            <br>
                            <button id="select-theme-downloader-button" onclick="theme_download_select()">Select Theme</button>
                        </div>
                        <div id="theme-downloader-description" class="column theme-description">
                            <h3>Theme Description</h3>
                            <div id="theme-downloader-description-content"><p>No description available</p></div>
                            <img id="repo_screenshot" src="/screenshots/screenshot.png" onerror="this.onerror=null; this.src='/screenshots/screenshot.png';"></img>
                        </div>
                    </div>
                </div>
            </div>
            <div id="config" class="ui-content">
                <h2>No configuration for the default theme</h2> <!-- Updated dynamically -->
                <div id="hidden">
                    <button onclick="saveConfig()" id="sticky-button">Save Configuration</button>
                    <h3>Configuration editor</h3>
                    <input type="text" id="configSearch" onkeyup="searchConfig()" placeholder="Search for options..." title="Type in a name">
                    <h4>Config Path</h4> <!-- Updated dynamically -->
                    <div id="config_content"></div> <!-- Config data inserted here dynamically -->
                    <h3>CSS editor</h3>
                    <h4>CSS Path</h4>  <!-- css path inserted here dynamically -->
                    <div contenteditable="true" id="CSS" class="config-box"></div> <!-- css content inserted here dynamically -->
                    <h3>Info editor</h3>
                    <h4>Info Path</h4>  <!-- css path inserted here dynamically -->
                    <div contenteditable="true" id="Info" class="config-box"></div> <!-- Info content inserted here dynamically -->
                </div>
                <button onclick="resetCSS()">Reset Pwnagotchi core CSS</button>
            </div>

            <div id="theme_editor" class="ui-content">
                
                <div id="fancygotchi">
                    <h2>Theme editor</h2>
                    <div id="theme_editor_content">
                        <h2>Coming soon !</h2>
                        <h2>If you like the project feel free to contribute !</h2>
                        <h2><a href='{{fancy_repo}}'>Fancygotchi</a> is made with ❤ by <a href='https://linktr.ee/v0r_t3x'>V0rT3x</a></h2>
                    </div>
                    <div id="logo">
                        <pre>
{% for line in logo.splitlines() %}<span>{{ line }}</span>
{% endfor %}
                        </pre>
                    </div>
                </div>
                <div id="theNet"><a onclick="theNet()">π</a></div>
            </div>
        </div>
        
        <div id="footer">
            <a href='{{fancy_repo}}'>Fancygotchi</a> {{ version }} made with ❤ by <a href='https://linktr.ee/v0r_t3x'>{{ author }}</a>
        </div>
        
    </div>
    <div data-role="popup" id="delete-dialog" data-overlay-theme="b" data-theme="b" data-dismissible="false" style="max-width:400px;">
        <div role="main" class="ui-content">
            <h3 class="ui-title">Confirm Deletion</h3>
            <p>Are you sure you want to delete the selected theme?</p>
            <a href="#" class="ui-btn ui-corner-all ui-shadow ui-btn-inline ui-btn-b" data-rel="back">Cancel</a>
            <a href="#" id="confirm-delete" class="ui-btn ui-corner-all ui-shadow ui-btn-inline ui-btn-b" data-rel="back">Delete</a>
        </div>
    </div>
    <button id="scrollToTopBtn" class="scroll-to-top-btn">&#x25B2;</button>
{% endblock %}
{% block script %}
theme_info("{{name}}");
loadConfig(0, "{{name}}");

var scrollToTopBtn = document.getElementById("scrollToTopBtn");

function theNet() {
    var div = document.querySelector(".dev");
    var logo = document.querySelector("#logo");

    if (!div || !logo) {
        console.error('Element not found: .dev or #logo');
        return;
    }

    var computedColor = window.getComputedStyle(logo).color;
    console.log(computedColor)

    function rgbToColor(rgb) {
        return rgb.replace(/\s+/g, '').toLowerCase();
    }

    var limeColor = rgbToColor("rgb(0, 255, 0)"); 

    if (div.style.display === "none" || div.style.display === "") {
        if (rgbToColor(computedColor) === limeColor) {
            logo.style.color = "red"; 
        } else {
            logo.style.color = "lime"; 
        }

        glitchEffect(true);
        div.style.display = "block";
        logo.style.backgroundColor = "black";
    } else {
        div.style.display = "none";
        logo.style.color = ""; 
        logo.style.backgroundColor = "";
    }
}

window.onload = function() {
    var image = document.getElementById("ui");
    var image2 = document.getElementById("ui2");
    function updateImage() {
        image.src = image.src.split("?")[0] + "?" + new Date().getTime();
        image2.src = image2.src.split("?")[0] + "?" + new Date().getTime();
    }
    setInterval(updateImage, {{webui_fps}});
}

$(document).ready(function () {
    // Variable to track the keyboard toggle state
    let keyboardActive = false;

    // Listen for changes on the keyboard toggle flipswitch
    $('#keyboard').on('change', function () {
        keyboardActive = $(this).is(':checked'); // Set true if checked, false otherwise
    });

    // Keydown event listener
    $(document).on('keydown', function (e) {
        if (!keyboardActive) return; // Exit if keyboard is toggled off
        switch (e.key) {
            case "ArrowUp":
                e.preventDefault(); 
                $('#up').click();
                break;
            case "ArrowDown":
                e.preventDefault(); 
                $('#down').click();
                break;
            case "ArrowLeft":
                e.preventDefault(); 
                $('#left').click();
                break;
            case "ArrowRight":
                e.preventDefault(); 
                $('#right').click();
                break;
            case "Enter":
                e.preventDefault(); 
                $('#select').click();
                break;
            case "s":
                $('#stealth').click();
                break;
            case "t":
                $('#start').click();
                break;
            case "a":
                $('#a').click();
                break;
            case "b":
                $('#b').click();
                break;
            case "x":
                $('#x').click();
                break;
            case "y":
                $('#y').click();
                break;
            case "Shift":
                e.preventDefault(); 
                $('#l1').click();
                break;
            case "Alt":
                e.preventDefault(); 
                $('#r1').click();
                break;
            case "Control":
                e.preventDefault(); 
                $('#l2').click();
                break;
            case " ":
                e.preventDefault(); 
                $('#r2').click();
                break;
        }
    });
});

window.onscroll = function() {
    if (document.body.scrollTop > 100 || document.documentElement.scrollTop > 100) {
        scrollToTopBtn.classList.add("show");
    } else {
        scrollToTopBtn.classList.remove("show");
    }
};

scrollToTopBtn.addEventListener("click", function() {
    window.scrollTo({top: 0, behavior: 'smooth'});
});

function searchConfig() {
    var input, filter, table, tr, i, td, txtValue;
    input = document.getElementById("configSearch");
    filter = input.value.toUpperCase();
    table = document.getElementById("tableOptions");
    if (!table) return;
    tr = table.getElementsByTagName("tr");

    // Loop through all table rows (except the header and the 'add' row), and hide those who don't match the search query
    for (i = 1; i < tr.length -1; i++) {
        td = tr[i].getElementsByTagName("td")[1]; // The second column contains the option name
        if (td) {
            txtValue = td.textContent || td.innerText;
            if (txtValue.toUpperCase().indexOf(filter) > -1) {
                tr[i].style.display = "";
            } else {
                tr[i].style.display = "none";
            }
        }
    }
}
function active_theme(callback) {
    loadJSON("Fancygotchi/active_theme", function(response) {
        callback(response.theme);
    });
}

function resetCSS() {
    loadJSON("Fancygotchi/reset_css", function(response) {
        console.log("CSS reset successful!");
        alert("CSS reset successful!");
    });
}

function theme_select() {
    var theme = document.getElementById("theme-selector").value;
    var rotation = document.getElementById("orientation-selector").value;
    
    //var json = {"theme": theme, "rotation": rotation};
    var url = "Fancygotchi/theme_select?theme="+theme+"&rotation="+rotation;
    console.log(url);
    loadJSON(url, function(response) {
        loadConfig(1, theme);
    }); 
}

function loadConfig(a, theme) {
    if (a == 1) {
        alert(theme + ' selected');
    }
    if (theme == "Default") {
        document.querySelector("#config h2").innerText = "No configuration for the default theme";
        document.getElementById("hidden").style.visibility = "hidden";
        document.getElementById("hidden").style.display = "none";
    } else {
        document.getElementById("hidden").style.visibility = "visible";
        document.getElementById("hidden").style.display = "inline-block";
    }
    loadJSON("Fancygotchi/load_config", function(response) {
        updateConfigSection(response);
    });
}

function escapeHtml(text) {
    return text
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
}

function updateConfigSection(data) {
    populateConfig(data.config)
    if (data.name == "Default"  || data.name == "") {
        document.querySelector("#config h2").innerText = "No configuration for the default theme";

    } else {
        document.querySelector("#config h2").innerText = "Configuration of " + data.name;

    }
    document.querySelector("#config h4:nth-of-type(1)").innerText = data.cfg_path;
    document.querySelector("#config h4:nth-of-type(2)").innerText = data.css_path;
    var cssContent = document.getElementById("CSS");
    cssContent.innerHTML = '<div class="preserve-line-breaks">' + escapeHtml(data.css) + '</div>';
    document.querySelector("#config h4:nth-of-type(3)").innerText = data.info_path;
    var infoContent = document.getElementById("Info");
    infoContent.innerHTML = '<div class="preserve-line-breaks">' + escapeHtml(data.info) + '</div>';
}

function populateConfig(config) {
    var configContent = $('#config_content');
    configContent.empty();
    var table = jsonToTable(flattenJson(config));
    configContent.append(table);
}

function jsonToTable(json) {
  var table = document.createElement("table");
  table.id = "tableOptions";

  var tr = table.insertRow();
  var thDel = document.createElement("th");
  thDel.innerHTML = "";
  var thOpt = document.createElement("th");
  thOpt.innerHTML = "Option";
  var thVal = document.createElement("th");
  thVal.innerHTML = "Value";
  tr.appendChild(thDel);
  tr.appendChild(thOpt);
  tr.appendChild(thVal);

  var td, divDelBtn, btnDel;
  Object.keys(json).forEach(function(key) {
    tr = table.insertRow();
    divDelBtn = document.createElement("div");
    divDelBtn.className = "del_btn_wrapper";
    td = document.createElement("td");
    td.setAttribute("data-label", "");
    if (!key.startsWith("theme.options")) {
      btnDel = document.createElement("Button");
      btnDel.innerHTML = "X";
      btnDel.setAttribute("data-key", key);
      btnDel.onclick = function(){ delRow(this);};
      btnDel.className = "remove";
      divDelBtn.appendChild(btnDel);
      td.appendChild(divDelBtn);
    }
    tr.appendChild(td);
    td = document.createElement("td");
    td.setAttribute("data-label", "Option");
    td.innerHTML = key;
    tr.appendChild(td);
    td = document.createElement("td");
    td.setAttribute("data-label", "Value");
    if(typeof(json[key])==='boolean'){
      var input = document.createElement("select");
      input.setAttribute("id", "boolSelect");
      var tvalue = document.createElement("option");
      tvalue.setAttribute("value", "true");
      var ttext = document.createTextNode("True")
      tvalue.appendChild(ttext);
      var fvalue = document.createElement("option");
      fvalue.setAttribute("value", "false");
      var ftext = document.createTextNode("False");
      fvalue.appendChild(ftext);
      input.appendChild(tvalue);
      input.appendChild(fvalue);
      input.value = json[key];
      td.appendChild(input);
    } else {
      var input = document.createElement("input");
      if(Array.isArray(json[key])) {
        input.type = 'text';
        input.value = '[' + json[key].join(', ') + ']';
      } else {
        input.type = typeof(json[key]);
        input.value = json[key];
      }
      td.appendChild(input);
    }
    tr.appendChild(td);
  });

  var newTr = table.insertRow();
  var newTd = newTr.insertCell();
  newTd.setAttribute("data-label", "");
  var addButton = document.createElement("button");
  addButton.innerHTML = "+";
  addButton.onclick = function() {
    var newRow = table.insertRow();
    var newTd = newRow.insertCell();
    var delButton = document.createElement("button");
    delButton.innerHTML = "X";
    delButton.onclick = function() {
      this.parentNode.parentNode.remove();
    };
    newTd.appendChild(delButton);
    var newKeyCell = newRow.insertCell();
    var newKeyInput = document.createElement("input");
    newKeyInput.type = "text";
    newKeyInput.placeholder = "New Key";
    newKeyCell.appendChild(newKeyInput);

    var newValueCell = newRow.insertCell();
    var newValueInput = document.createElement("input");
    newValueInput.type = "text";
    newValueInput.placeholder = "New Value";
    newValueCell.appendChild(newValueInput);
  };
  newTd.appendChild(addButton);
  newTr.appendChild(newTd);
  newTr.appendChild(document.createElement("td"));

  return table;
}
function delRow(btn) {
    var key = btn.getAttribute("data-key");
    var tr = btn.closest("tr");
    if (tr && key) {
        tr.parentNode.removeChild(tr);
    }
}

function saveConfig() {
    var config = document.getElementById("tableOptions");
    var css = document.getElementById("CSS").textContent;
    var info = document.getElementById("Info").textContent;

    console.log(info)
    console.log(css)

    var data = {
        config: tableToJson(config),
        css: css,
        info: info
    };
    sendJSON("Fancygotchi/save_config", data, function(response) {
        if (response.status == "200") {
            alert("Config got updated");
        } else {
            alert("Error while updating the config (err-code: " + response.status + ")");
        }
    });
    active_theme(function(activeTheme) {
        loadConfig(0, activeTheme)
        theme_info(activeTheme)
    });
}

function tableToJson(table) {
  var rows = table.getElementsByTagName("tr");
  var i, td, key, value;
  var json = {};

  for (i = 0; i < rows.length; i++) {
    td = rows[i].getElementsByTagName("td");
    if (td.length == 3) {
      key = td[1].textContent || td[1].innerText;
      console.log(td[1].textContent || td[1].innerText);
      var input = td[2].getElementsByTagName("input");
      var select = td[2].getElementsByTagName("select");
      console.log(key);

      if (input && input.length > 0) {
        if (input[0].type == "text") {
          const inputValue = input[0].value.trim();
          if (inputValue === "") {
            value = ""; 
          } else if (inputValue.startsWith("[") && inputValue.endsWith("]")) {
            try {
              value = JSON.parse(inputValue); 
            } catch (e) {
              console.error('Invalid JSON array:', inputValue);
              value = inputValue;
            }
          } else if (inputValue === 'true' || inputValue === 'false') {
            value = inputValue === 'true'; 
          } else if (!isNaN(inputValue)) {
            value = parseInt(inputValue, 10); 
          } else {
            value = inputValue;
          }
        } else if (input[0].type == "number") {
          value = Number(input[0].value); 
        }
      } else if (select && select.length > 0) {
        value = select[0].options[select[0].selectedIndex].value === 'true';
      }

      var keyParts = key.split('.');
      var currentObj = json;
      for (var j = 0; j < keyParts.length - 1; j++) {
        if (!currentObj[keyParts[j]]) {
          currentObj[keyParts[j]] = {}; 
        }
        currentObj = currentObj[keyParts[j]];
      }
      currentObj[keyParts[keyParts.length - 1]] = value;
    }
  }

  var newRows = document.querySelectorAll("tr input[type='text'][placeholder='New Key']");
  newRows.forEach(function(newKeyInput) {
    var newValueInput = newKeyInput.closest("tr").querySelector("input[placeholder='New Value']");
    var newKey = newKeyInput.value.trim();
    var newValue = newValueInput.value.trim();

    
  if (newKey) {
    if (newValue === "") {
      newValue = "";
    } else if (newValue.startsWith("[") && newValue.endsWith("]")) {
      try {
        newValue = JSON.parse(newValue);
      } catch (e) {
        console.error('Invalid JSON array:', newValue);
        newValue = newValue;
      }
    } else if (newValue === 'true' || newValue === 'false') {
      newValue = newValue === 'true';
    } else if (!isNaN(newValue)) {
      newValue = parseFloat(newValue);
    } else {
      newValue = newValue;
    }

      var newKeyParts = newKey.split('.');
      var currentNewObj = json;
        console.log(newKeyParts)
      for (var k = 0; k < newKeyParts.length - 1; k++) {
        if (!currentNewObj[newKeyParts[k]]) {
          currentNewObj[newKeyParts[k]] = {};
        }
        currentNewObj = currentNewObj[newKeyParts[k]];
      }
      currentNewObj[newKeyParts[newKeyParts.length - 1]] = newValue;
    }
  });

  return unFlattenJson(json);
}

function unFlattenJson(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var result = {}, cur, prop, idx, last, temp, inarray;
    for(var p in data) {
        cur = result, prop = "", last = 0, inarray = false;
        do {
            idx = p.indexOf(".", last);
            temp = p.substring(last, idx !== -1 ? idx : undefined);
            inarray = temp.startsWith('#') && !isNaN(parseInt(temp.substring(1)))
            cur = cur[prop] || (cur[prop] = (inarray ? [] : {}));
            if (inarray){
                prop = temp.substring(1);
            }else{
                prop = temp;
            }
            last = idx + 1;
        } while(idx >= 0);
        cur[prop] = data[p];
    }
    return result[""];
}

function createNewTheme() {
    var themeName = document.getElementById("new-theme-name").value;
    var useResolution = document.getElementById("use-resolution").checked;
    var useOrientation = document.getElementById("use-orientation").checked;
    if (!themeName) {
        alert("Please enter a theme name");
        return;
    }
    var json = {
        "theme_name": themeName,
        "use_resolution": useResolution,
        "use_orientation": useOrientation
    };
    sendJSON("Fancygotchi/create_theme", json, function(response) {
        if (response.status == 200) {
            alert("Theme created successfully");
            theme_list();
        } else {
            alert("Error creating theme: " + response.responseText);
        }
    });
}

function copyTheme() {
    var theme = document.getElementById("theme-selector").value;
    if (theme != "Default") {
        if (theme) {
            var newName = theme + '-copy';
            sendJSON("Fancygotchi/theme_copy", {"theme": theme, "new_name": newName}, function(response) {
                if (response.status == 200) {
                    alert("Theme copied successfully");
                    theme_list();
                } else {
                    alert("Error copying theme: " + response.responseText);
                }
            });
        } else {
            alert('Please select a theme to copy.');
        }
    } else {
        alert('Default theme cannot be copied.');
    }
}

function renameTheme() {
    var theme = document.getElementById("theme-selector").value;

    active_theme(function(activeTheme) {
        if (theme !== "Default" && theme !== activeTheme) {
            if (theme) {
                var newName = prompt("Enter new name for the theme:", theme);
                if (newName && newName !== theme) {
                    sendJSON("Fancygotchi/theme_rename", {"theme": theme, "new_name": newName}, function(response) {
                        if (response.status == 200) {
                            alert("Theme renamed successfully");
                            theme_list(); 
                        } else {
                            alert("Error renaming theme: " + response.responseText);
                        }
                    });
                }
            } else {
                alert('Please select a theme to rename.');
            }
        } else {
            alert('Default theme or active theme cannot be renamed.');
        }
    });
}

function theme_upload(event) {
    event.preventDefault();

    var formData = new FormData();
    var fileInput = document.getElementById('zipFile');
    var file = fileInput.files[0];

    if (!file) {
        alert('No file selected.');
        return;
    }

    formData.append('zipFile', file);

    sendFormData('Fancygotchi/theme_upload', formData, function(err, response) {
        if (err) {
            console.error(err);
            alert('An error occurred while uploading the theme.');
            return;
        }

        if (response.startsWith('Zip file uploaded')) {
            alert(response);
            theme_list();
        } else if (response.startsWith('Some folders were not copied')) {
            alert(response);
        } else {
            alert('Error: ' + response);
        }
    });
}

function theme_export() {
    var selectedTheme = document.getElementById('theme-selector').value;
    if (selectedTheme != "Default") {
        if (selectedTheme) {
            window.location.href = 'Fancygotchi/theme_export/' + selectedTheme;
        } else {
            alert('Please select a theme to export.');
        }
    } else {
        alert('Default theme cannot be exported.');
    }
}
$(document).on('click', '#confirm-delete', function() {
    var theme = $('#theme-selector').val();
    
    if (theme != "Default") {
        var json = { "theme": theme };
        
        sendJSON("Fancygotchi/theme_delete", json, function(xhr) {
            if (xhr.status == 200) {
                theme_list();
            }
        });
    } else {
        alert('Default theme cannot be deleted.');
    }
});

function theme_list() {

    active_theme(function(activeTheme) {
        loadJSON("Fancygotchi/theme_list", function(response) {
            populateThemeSelector(response, activeTheme);
        });
        $('#theme-selector').val(activeTheme);
        theme_info(activeTheme);
    });
}
function theme_info(activeTheme) {
    var theme = $('#theme-selector').val();
    var json = { "theme": theme };
    sendJSON("Fancygotchi/theme_info", json, function(xhr) {
        if (xhr.status == 200) {
            var themeInfo = JSON.parse(xhr.responseText);
            console.log(themeInfo);
            populateThemeInfo(themeInfo);
        }
    });
}
$('#theme-selector').change(function() {
    theme_info($(this).val());
});
function populateThemeSelector(themes, activeTheme) {
    var selectElement = $('#theme-selector');
    selectElement.empty();
    

    var defaultOption = $('<option>').val('Default').text('Default');
    selectElement.append(defaultOption);
    
    themes.forEach(function(theme) {
        var option = $('<option>').val(theme).text(theme);
        if (theme === activeTheme) {
            option.attr('selected', 'selected');
        }
        selectElement.append(option);
    });
    
    if (!themes.includes('Default') && !activeTheme) {
        defaultOption.attr('selected', 'selected');
    }
    
    selectElement.selectmenu('refresh');
    return activeTheme

}
function populateThemeInfo(themeInfo) {
    var $themeDescriptionContent = $('#theme-description-content');

    active_theme(function(activeTheme) {
        var theme = $('#theme-selector').val() || activeTheme || 'Default';
        console.log(theme);

        $themeDescriptionContent.empty();
        $themeDescriptionContent.append('<h3>' + theme.toUpperCase() + '</h3>');

        var $screenshot = $('#screenshot');
        var screenshotSrc = $('#theme-selector').val() == activeTheme 
            ? '/img/screenshot.png' 
            : '/screenshots/' + theme + '/screenshot.png';

        // Add a cache-busting timestamp parameter
        $screenshot.attr('src', screenshotSrc + '?cache_buster=' + new Date().getTime());
        
        // Log the src attribute to check the updated URL
        console.log($screenshot.attr('src'));

        $screenshot.on('error', function() {
            $(this).attr('src', '/screenshots/screenshot.png?cache_buster=' + new Date().getTime());
        });

        Object.entries(themeInfo).forEach(([key, value]) => {
            var val = '<span class="preserve-line-breaks">' + value + '</span>';
            $themeDescriptionContent.append($('<li>').html(key + ': ' + val));
        });
    });
}



function loadThemeRepo() {
    $('#theme_downloader').find('select, button').prop('disabled', true);
    $('#loading-spinner').show();
    $('#download_window').hide();
    $('#loading-spinner p').text("Loading...");

    loadJSON("Fancygotchi/theme_download_list", function(response) {
        console.log(response.status);
        if (response.status == 200) {
            populateThemeSelector_downloader(response.data);
            $('#loading-spinner').hide();
            $('#download_window').show();
            $('#theme_downloader').find('select, button').prop('disabled', false);
        }
        else {
            var error = response.error || "An error occurred";
            $('#loading-spinner p').text(error);
            $('#theme_downloader').find('select, button').prop('disabled', false);
        }
    });
}

$('#theme-downloader-selector').change(function() {
    var themes = window.themes; 
    var selectedTheme = $('#theme-downloader-selector').val();
    populateThemeInfo_downloader(themes[selectedTheme]);
});

function populateThemeSelector_downloader(themes) {
    window.themes = themes; 
    var selectElement = $('#theme-downloader-selector');
    selectElement.empty();
    const sortedThemes = Object.keys(themes).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
    sortedThemes.forEach(function(theme) {
        var option = $('<option>').val(theme).text(theme);
        selectElement.append(option);
    });
    selectElement.selectmenu('refresh');
    if (sortedThemes.length > 0) {
        populateThemeInfo_downloader(themes[sortedThemes[0]]);
    }
}

function populateThemeInfo_downloader(themeInfo) {
    var $themeDescriptionContent = $('#theme-downloader-description-content');
    var theme = $('#theme-downloader-selector').val();
    if (theme == '') {
        theme = 'Default';
    }
    $themeDescriptionContent.empty();
    $themeDescriptionContent.append('<h3>' + theme.toUpperCase() + '</h3>');
    var img = new Image();
    var imgPath = '/repo_screenshots/' + theme + '/screenshot.png';
    img.onload = function() {
        document.getElementById('repo_screenshot').src = imgPath;
    };
    img.onerror = function() {
        document.getElementById('repo_screenshot').src = '/repo_screenshots/screenshot.png';
    };
    img.src = imgPath;
    $.each(themeInfo.info, function(key, value) {
        var val = '<span class="preserve-line-breaks">' + value + '</span>';
        var listItem = $('<li>').html(key + ': ' + val);
        $themeDescriptionContent.append(listItem);
    });
}

function theme_download_select() {
    var theme = document.getElementById("theme-downloader-selector").value;
    
    $('#theme_downloader').find('select, button').prop('disabled', true);

    var themes = window.themes; 
    var version = themes[theme]?.info?.version || 'Unknown'; 
    var json = {
        "theme": theme,
        "version": version
    };
    sendJSON("Fancygotchi/version_compare", json, function(response) {
        data = JSON.parse(response.responseText)
        var localVersion = data.local_version || 'Unknown';
        var isNewer = data.is_newer;
        if (isNewer) {
            var message = `A newer ${theme} version (${version}) is available. Your current version is ${localVersion}. Would you like to update?`;
        } 
        if (!isNewer) {
            var message = `You have the ${theme} version ${localVersion} installed. The available version is ${version}. Do you want to overwrite your current version?`;
        }
        if (isNewer == null) {
            var message = `You will download ${theme} version ${version}. Do you want to peoceed?`;
        }
        var confirmOverwrite = confirm(message);
        if (confirmOverwrite) {
            var json = {
                "theme": theme,
            };
            $('#loading-spinner').show();
            $('#loading-spinner p').text("Downloading...");
            sendJSON("Fancygotchi/theme_download_select", json, function(response) {
                if (response.status == 200) {
                    $('#loading-spinner').hide();
                    alert("Theme updated successfully!");
                    theme_list();
                } else {
                    $('#loading-spinner').hide();
                    alert("There was an error updating the theme.");
                }
                $('#theme_downloader').find('select, button').prop('disabled', false);
            });
        }
    });
}

function sendJSON(url, data, callback) {
    var xobj = new XMLHttpRequest();
    var csrf = "{{ csrf_token() }}";
    xobj.open('POST', url);
    xobj.setRequestHeader("Content-Type", "application/json");
    xobj.setRequestHeader('x-csrf-token', csrf);
    xobj.onreadystatechange = function () {
        if (xobj.readyState == 4) {
            callback(xobj);
        }
    };
    xobj.send(JSON.stringify(data));
}

function sendFormData(url, formData, callback) {
    var xhr = new XMLHttpRequest();
    var csrf = "{{ csrf_token() }}";
    xhr.open('POST', url);
    xhr.setRequestHeader('x-csrf-token', csrf);
    xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            console.log("Response received:", xhr.responseText); 
            if (xhr.status === 200) {
                document.getElementById('zipFile').value = '';
                document.getElementById('message').innerHTML = 'Upload successful!';
                callback(null, xhr.responseText);
            } else {
                console.error('Request failed with status:', xhr.status);
                document.getElementById('message').innerHTML = 'Upload error: ' + xhr.responseText;
                callback(new Error('Request failed with status: ' + xhr.status), null);
            }
        }
    };

    xhr.send(formData);
}

function loadJSON(url, callback) {
    var xobj = new XMLHttpRequest();
    xobj.overrideMimeType("application/json");
    xobj.open('GET', url, true);
    xobj.onreadystatechange = function () {
        if (xobj.readyState == 4 && xobj.status == "200") {
            callback(JSON.parse(xobj.responseText));
        }
        if (xobj.readyState == 4 && xobj.status == "500") {
            callback(JSON.parse(xobj.responseText));
        }
        if (xobj.readyState == 4 && xobj.status == "404") {
            callback(JSON.parse(xobj.responseText));
        }
    };
    xobj.send(null);
}

function flattenJson(data) {
    var result = {};

    function recurse(cur, prop) {
        if (Array.isArray(cur)) {
            result[prop] = cur.map(function(item) {
                return typeof item === 'string' ? `"${item}"` : item;
            });
        } else if (Object(cur) !== cur) {
            result[prop] = cur;
        } else {
            for (var p in cur) {
                recurse(cur[p], prop ? prop + "." + p : p);
            }
        }
    }

    recurse(data, "");
    return result;
}

function jsonToArray(json) {
    var theme_array = [];
    var x = 0;
    Object.keys(json).forEach(function(key) {
        theme_array[x] = [key, json[key]];
        x+=1;
    });
    return theme_array;
}
function openDeleteDialog() {
    $('#delete-dialog').popup('open');
}
function theme_delete() {
    var theme = document.getElementById("theme-selector").value;
    active_theme(function(activeTheme) {
        if (theme !== "Default" && theme !== activeTheme) {
            openDeleteDialog();
        } else {
            alert('Cannot delete default theme or the active theme.');
        }
    });
}

function display_hijack() {
    loadJSON("Fancygotchi/display_hijack",function(response) {
        if (response.status == "200") {
            alert("Screen Hijacked");
        } else {
            alert("Error while hijacking the display (err-code: " + response.status + ")");
        }
    });
}
function display_pwny() {
    loadJSON("Fancygotchi/display_pwny", function(response) {
        if (response.status == "200") {
            alert("Screen Pwny");
        } else {
            alert("Error while diplaying pwagotchi (err-code: " + response.status + ")");
        }
    });
}
function display_next() {
    loadJSON("Fancygotchi/display_next",function(response) {
        if (response.status == "200") {
            alert("Next second screen mode");
        } else {
            alert("Error while diplaying next second screen mode (err-code: " + response.status + ")");
        }
    });
}
function display_previous() {
    loadJSON("Fancygotchi/display_previous", function(response) {
        if (response.status == "200") {
            alert("Next second screen mode");
        } else {
            alert("Error while diplaying previous second screen mode (err-code: " + response.status + ")");
        }
    });
}
function screen_saver_next() {
    loadJSON("Fancygotchi/screen_saver_next", function(response) {
        if (response.status == "200") {
            alert("Next screen saver");
        } else {
            alert("Error while diplaying next screen saver (err-code: " + response.status + ")");
        }
    });
}
function screen_saver_previous() {
    loadJSON("Fancygotchi/screen_saver_previous", function(response) {
        if (response.status == "200") {
            alert("Previous screen saver");
        } else {
            alert("Error while diplaying previous screen saver (err-code: " + response.status + ")");
        }
    });
}
function stealth() {
    loadJSON("Fancygotchi/stealth", function(response) {
        if (response.status == "200") {
            alert("Stealth mode");
        } else {
            alert("Error while enabling stealth mode (err-code: " + response.status + ")");
        }
    });
}
function navigate(btn) {
    var action = btn
    var which_screen = document.getElementById("screen").checked;
    var screen = 1
    if (which_screen == true) {
        screen = 2
    }
    console.log("screen: "+screen)
    loadJSON("Fancygotchi/btn_cmd?action="+action+"&hardware=False&screen="+screen,  function(response) {
        if (response.status == "200") {
            console.log("Navigation: " + "Fancygotchi/btn_cmd?action="+action+"&hardware=False&screen="+screen);
        } else {
            console.log("Navigation error: " + btn + " (err-code: " + response.status + ")");
        }
    });
}

function glitchEffect(amplify = false) {
    const lines = document.querySelectorAll('#fancygotchi span');
    const numLines = lines.length;
    
    const numGlitches = amplify ? Math.floor(Math.random() * numLines) + 1 : Math.min(5, Math.floor(Math.random() * 5));
    const indices = new Set();
    
    while (indices.size < numGlitches) {
        indices.add(Math.floor(Math.random() * numLines));
    }
    
    indices.forEach(index => {
        const line = lines[index];
        line.classList.add('glitch-line');
        
        const randomMove = Math.floor(Math.random() * 200) - 50; 
        line.style.transform = `translateX(${randomMove}px)`;  
        
        setTimeout(() => {
            line.classList.remove('glitch-line');
            line.style.transform = ''; 
        }, amplify ? 1000 : 300);
    });
}
document.addEventListener('click', () => {
    glitchEffect(true);
});

setInterval(glitchEffect, 150);
{% endblock %}
"""

CSS = """
.ui-image {
    width: 100%;
}

.pixelated {
    image-rendering: optimizeSpeed; /* Legal fallback */
    image-rendering: -moz-crisp-edges; /* Firefox        */
    image-rendering: -o-crisp-edges; /* Opera          */
    image-rendering: -webkit-optimize-contrast; /* Safari         */
    image-rendering: optimize-contrast; /* CSS3 Proposed  */
    image-rendering: crisp-edges; /* CSS4 Proposed  */
    image-rendering: pixelated; /* CSS4 Proposed  */
    -ms-interpolation-mode: nearest-neighbor; /* IE8+           */
}

.image-wrapper {
    flex: 1;
    position: relative;
}

div.status {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
}

a.read {
    color: #777 !important;
}

p.messagebody {
    padding: 1em;
}

li.navitem {
    width: 16.66% !important;
    clear: none !important;
}

/* Custom indentations are needed because the length of custom labels differs from
   the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {
    text-indent: -5.9em;
}

.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {
    text-indent: 0.5em;
}

/* Custom widths are needed because the length of custom labels differs from
   the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch {
    width: 8.875em;
}

.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {
    padding-left: 7em;
    width: 1.875em;
}

@media (min-width: 28em) {
    /*Repeated from rule .ui-flipswitch above*/
    .ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {
        width: 1.875em;
    }
}

#container {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
}

.plugins-box {
    margin: 0.5rem;
    padding: 0.2rem;
    border-style: groove;
    border-radius: 0.5rem;
    background-color: lightgrey;
    text-align: center;
}             
"""

BOOT_ANIM = """import time
from PIL import Image, ImageSequence
import os
import logging
from pwnagotchi import utils
import pwnagotchi.ui.hw as hw

from pwnagotchi.ui.hw import display_for
import argparse
#import traceback

def setup_logging(log_file='/var/log/bootanim.log'):
    # Ensure the directory exists
    log_dir = os.path.dirname(log_file)
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)

    # Configure logging
    logging.basicConfig(
        filename=log_file,
        level=logging.INFO,  # or DEBUG, WARNING, ERROR, CRITICAL
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

def show_boot_animation(display_driver, config):
    try:
        frames_path = '{img_path}'
        width = {width}
        height = {height}
        rotation = {rotation}

        # Check if folder exists
        if not os.path.exists(frames_path):
            return

        # Accept common image formats
        valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif')
        frames = sorted([f for f in os.listdir(frames_path) if f.lower().endswith(valid_extensions)])
        logging.debug("Found %s frames" % len(frames))
        # Check if there are any images to process
        if not frames:
            return

        frames_count = len(frames)

        if len(frames) == 1:
            if frames[0].lower().endswith('.gif'):
                source_path = os.path.join(frames_path, frames[0])
                with Image.open(source_path) as img:
                    frames_count = sum(1 for _ in ImageSequence.Iterator(img))

        max_loops = {max_loops}
        total_duration = {total_duration}
        start_time = time.time()
        loop_count = 0

        delay =  total_duration / (frames_count * max_loops)

        while (time.time() - start_time < total_duration) or (loop_count < max_loops):
            for frame in frames:
                if (time.time() - start_time >= total_duration) and (loop_count >= max_loops):
                    break
                
                frame_path = os.path.join(frames_path, frame)

                if frame.lower().endswith('.gif'):
                    logging.debug('Processing GIF: %s' % frame_path)
                    with Image.open(frame_path) as img:
                        for gif_frame in ImageSequence.Iterator(img):
                            if rotation == 90:
                                gif_frame = gif_frame.rotate(90, expand=True)
                            elif rotation == 180:
                                gif_frame = gif_frame.rotate(180, expand=True)
                            elif rotation == 270:
                                gif_frame = gif_frame.rotate(270, expand=True)
                            gif_frame = gif_frame.resize((width, height)).convert('{color_mode}')
                            logging.debug('Rendering frame: %s' % gif_frame)
                            display_driver.render(gif_frame)
                else:
                    # Handle any other image formats (jpg, jpeg, bmp, png, etc.)
                    with Image.open(frame_path) as img:
                        if rotation == 90:
                            img = img.rotate(90, expand=True)
                        elif rotation == 180:
                            img = img.rotate(180, expand=True)
                        elif rotation == 270:
                            img = img.rotate(270, expand=True)
                        img = img.resize((width, height)).convert('{color_mode}')
                        logging.debug('Rendering frame: %s' % img)
                        display_driver.render(img)
                        
                time.sleep(delay)  # Adjust this value to control animation speed
            logging.debug('Finished loop %d' % loop_count)
            loop_count += 1
            
    except Exception as ex:
        logging.error(ex)
        #logging.error(traceback.format_exc())
        display_driver.clear()

if __name__ == "__main__":
    setup_logging()
    logging.debug('Starting boot animation...')
    args = argparse.Namespace(
        config='/etc/pwnagotchi/default.toml', 
        user_config='/etc/pwnagotchi/config.toml', 
        do_manual=False, 
        skip_session=False, 
        do_clear=False, 
        debug=False, 
        version=False, 
        print_config=False, 
        wizard=False, 
        check_update=False, 
        donate=False
    )
    logging.debug(args)
    try:
        config = utils.load_config(args)
        logging.debug('Display config: %s' % config['ui']['display'])
        logging.debug('Display type: %s' % config['ui'])
        display_type = config['ui']['display']['type']
        logging.debug('Display type: %s' % display_type)
        display_driver = display_for(config)
        logging.debug(vars(display_driver))
        if display_driver is not None:
            
            display_driver.config['rotation'] = {rotation}
            
            if hasattr(display_driver, 'initialize'):
                try:
                    display_driver.initialize()
                    show_boot_animation(display_driver, config)
                    display_driver.config['enabled'] = True
                    display_driver.is_initialized = True
                except Exception as e:
                    logging.error(e)
            display_driver.config['enabled'] = False

            if hasattr(display_driver, 'displayImpl') and display_driver.config.get('enabled', False):
                display_driver.config['enabled'] = False
                logging.debug('[Fancygotchi] Display has been disabled')
                
                if hasattr(display_driver, 'clear'):
                    logging.debug('[Fancygotchi] Clearing the display')
                    display_driver.clear()
                
                display_driver.is_initialized = False

                if hasattr(display_driver, '_display'):
                    logging.debug('[Fancygotchi] Resetting internal display reference')
                    display_driver._display = None
        else:
            logging.error("Failed to initialize the display driver.")
    except KeyError as e:
        logging.error('KeyError: %s' % e)
        #logging.error(traceback.format_exc())
        display_type = 'Unknown'
"""

FANCYTOOLS = """#!{pyenv}
import time
import argparse
import os
import json
import subprocess
import requests
import toml

def create_log_directory():
    log_dir = '/var/log/fancytools/'
    if not os.path.exists(log_dir):
        result = subprocess.run(['sudo', 'mkdir', '-p', log_dir], check=True, capture_output=True, text=True)
        print("Directory %s created." % log_dir)
    return log_dir

def get_credentials():
    try:
        with open('/etc/pwnagotchi/config.toml', 'r') as f:
            config = toml.load(f)
            return (config['ui']['web']['username'], config['ui']['web']['password'])
    except:
        return ('changeme', 'changeme')

def send_command(command_data):
    username, password = get_credentials()
    base_url = 'http://%s:%s@localhost:8080/plugins/Fancygotchi' % (username, password)

    endpoint_map = {
        'stealth_mode': '/stealth',
        'second_screen': '/second_screen',
        'switch_screen_mode': '/display_next',
        'switch_screen_mode_reverse': '/display_previous',
        'next_screen_saver': '/screen_saver_next',
        'previous_screen_saver': '/screen_saver_previous',
        'up': '/btn_cmd',
        'down': '/btn_cmd',
        'left': '/btn_cmd',
        'right': '/btn_cmd',
        'select': '/btn_cmd',
        'start': '/btn_cmd',
        'a': '/btn_cmd',
        'b': '/btn_cmd',
        'x': '/btn_cmd',
        'y': '/btn_cmd',
        'l1': '/btn_cmd',
        'l2': '/btn_cmd',
        'r1': '/btn_cmd',
        'r2': '/btn_cmd',
        'theme_select': '/theme_select',
        'theme_refresh': '/theme_refresh',
        'plugin': '/plugin',
        'restart-auto': '/restart',
        'restart-manu': '/restart',
        'reboot-auto': '/reboot',
        'reboot-manu': '/reboot',
        'shutdown': '/shutdown'
    }

    action = command_data['action']
    endpoint = endpoint_map.get(action)

    if endpoint:
        try:
            query_params = ''
            for key, value in command_data.items():
                query_params += '%s=%s&' % (key, value)
            query_params = query_params[:-1]  # remove trailing &
            url = "%s%s?%s" % (base_url, endpoint, query_params)
            print(url)
            response = requests.get(url)

            if response.status_code == 200:
                print("Success: %s" % action)
            else:
                print("Error: %s - %s" % (response.status_code, response.text))

        except Exception as e:
            print("Error sending command: %s" % e)
            time.sleep(5)

def main():
    parser = argparse.ArgumentParser(description="Fancytools")
    parser.add_argument('-d', '--diagnostic', nargs='*', dest='diagnostic_args',
                        help='A full anonymized system report will be prompted. Additional arguments are accepted.')
    parser.add_argument('-p', '--plugin', dest='plugin', help='Name of the plugin to toggle')
    parser.add_argument('-e', '--enable', action='store_true', dest='enable',
                        help='Enable the specified plugin (default is to disable)')
    parser.add_argument('-r', '--restart', nargs='?', const='normal', dest='restart_mode',
                        help='Restart the system (auto or manu)')
    parser.add_argument('-b', '--reboot', nargs='?', const='normal', dest='reboot_mode',
                        help='Reboot the system (auto or manu)')
    parser.add_argument('-s', '--shutdown', action='store_true', dest='shutdown',
                        help='Shutdown the system')
    parser.add_argument('-B', '--button', choices=['start', 'up', 'down', 'left', 'right', 'select'], help='Control the menu')
    parser.add_argument('-pr', '--refresh-plugins', action='store_true', help='Refresh installed plugins list')
    parser.add_argument('-ts', '--theme-select', nargs=2, metavar=('NAME', 'ROTATION'), help='Select theme')
    parser.add_argument('-tr', '--theme-refresh', action='store_true', help='Refresh theme')
    parser.add_argument('-S', '--stealth-mode', action='store_true', help='Toggle stealth mode')
    parser.add_argument('-sw', '--switch-screen-mode', choices=['next', 'previous'], help='Switch screen mode')
    parser.add_argument('-s2', '--second-screen', action='store_true', help='Switch to second screen')
    parser.add_argument('-sc', '--screen-saver', choices=['next', 'previous'], help='Switch screen saver')
    parser.add_argument('-rb', '--run-bash', metavar='SCRIPT', help='Run a bash script')
    parser.add_argument('-rp', '--run-python', metavar='FILE', help='Run a Python script')
    
    args = parser.parse_args()

    log_dir = create_log_directory()

    if args.diagnostic_args is not None:
        script_path = os.path.abspath(__file__)
        print("The path of the running script is: %s" % script_path)
        path = "/usr/local/bin/diagnostic.sh"
        os.system(path)

    if args.plugin:
        enable_state = 'True' if args.enable else 'False'
        command_data = {
            'action': 'plugin',
            'name': args.plugin,
            'enable': enable_state
        }
        print(command_data)
        send_command(command_data)

    if args.restart_mode:
        send_command({'action': f'restart-{args.restart_mode}'})

    if args.reboot_mode:
        send_command({'action': f'reboot-{args.reboot_mode}'})

    if args.shutdown:
        send_command({'action': 'shutdown'})

    if args.button:
        send_command({'action': args.button, 'hardware': True})

    if args.refresh_plugins:
        send_command({'action': 'refresh_plugins'})

    if args.theme_select:
        send_command({'action': 'theme_select', 'name': args.theme_select[0], 'rotation': args.theme_select[1]})

    if args.theme_refresh:
        send_command({'action': 'theme_refresh'})

    if args.stealth_mode:
        send_command({'action': 'stealth_mode'})

    if args.switch_screen_mode:
        action = 'switch_screen_mode' if args.switch_screen_mode == 'next' else 'switch_screen_mode_reverse'
        send_command({'action': action})

    if args.second_screen:
        send_command({'action': 'second_screen'})

    if args.screen_saver:
        action = 'next_screen_saver' if args.screen_saver == 'next' else 'previous_screen_saver'
        send_command({'action': action})

    if args.run_bash:
        send_command({'action': 'run_bash', 'file': args.run_bash})

    if args.run_python:
        send_command({'action': 'run_python', 'file': args.run_python})

if __name__ == "__main__":
    main()
"""

DIAGNOSTIC= """#!/bin/bash

get_log_file_path() {
  local config_file="$1"
  if [ -f "$config_file" ]; then
    log_path=$(grep '^main\.log\.path ' "$config_file" | cut -d'=' -f2 | tr -d ' "')
    if [ -n "$log_path" ]; then
      echo "$log_path"
      return
    fi
  fi
  echo ""
}

# Get the script's directory
script_dir=$(dirname "$(readlink -f "$0")")

# Output file in the script's directory
output_file="/var/log/fancytools/system_info.txt"

# Pwnagotchi version
echo "Pwnagotchi version:" > "$output_file"
pip list | grep pwnagotchi >> "$output_file"
echo >> "$output_file"

# Kernel info
echo "Kernel info:" >> "$output_file"
uname -a >> "$output_file"
echo >> "$output_file"

# Boot config
echo "Boot config:" >> "$output_file"

# Check for the presence of cmdline.txt and config.txt in /boot/firmware
cmdline_file="/boot/firmware/cmdline.txt"
config_file="/boot/firmware/config.txt"

if [ -f "$cmdline_file" ]; then
  cat "$cmdline_file" >> "$output_file"
else
  # Fallback to /boot if not found in /boot/firmware
  if [ -f "/boot/cmdline.txt" ]; then
    cat "/boot/cmdline.txt" >> "$output_file"
  else
    echo "cmdline.txt not found." >> "$output_file"
  fi
fi
echo >> "$output_file"
if [ -f "$config_file" ]; then
  cat "$config_file" >> "$output_file"
else
  # Fallback to /boot if not found in /boot/firmware
  if [ -f "/boot/config.txt" ]; then
    cat "/boot/config.txt" >> "$output_file"
  else
    echo "config.txt not found." >> "$output_file"
  fi
fi
echo >> "$output_file"

# Service status
echo "Service status:" >> "$output_file"
service pwnagotchi status >> "$output_file"
echo >> "$output_file"

# Network driver interface load
echo "Network driver interface load:" >> "$output_file"
sudo dmesg | grep brcm >> "$output_file"
echo >> "$output_file"

# List all IP active host names
echo "List all IP active host names:" >> "$output_file"
hostname -I >> "$output_file"
echo >> "$output_file"

echo "List all active ports:" >> "$output_file"
lsof -nP -iTCP -sTCP:LISTEN >> "$output_file"
echo >> "$output_file"

# List available plugins
echo "List available plugins:" >> "$output_file"
cat /etc/pwnagotchi/config.toml | grep plugin | grep enabled >> "$output_file"
echo >> "$output_file"

# List enabled plugins
echo "List enabled plugins:" >> "$output_file"
cat /etc/pwnagotchi/config.toml | grep plugin | grep enabled | grep true >> "$output_file"
echo >> "$output_file"

# Attempt to find the log file path in the preferred config file
log_file=$(get_log_file_path "/etc/pwnagotchi/config.toml")

# If not found, check the default config file
if [ -z "$log_file" ]; then
  log_file=$(get_log_file_path "/etc/pwnagotchi/default.toml")
fi

# Check if log file path was found
if [ -z "$log_file" ]; then
  log_file="/var/log/pwnagotchi.log"
fi

# Config file
config_file="/etc/pwnagotchi/config.toml"

# Output files in the /var/log directory
log_dir="/var/log/fancytools/"
if [ ! -d "$log_dir" ]; then
  echo "Creating log directory: $log_dir"
  sudo mkdir -p "$log_dir" || { echo "Failed to create $log_dir"; exit 1; }
  echo "Directory $log_dir created."
fi

log_output_file="$log_dir/anonymized_log.txt"
config_output_file="$log_dir/anonymized_config.toml"

# Ensure we have write access to log files
if [ ! -w "$log_dir" ]; then
  echo "Cannot write to $log_dir. Please check permissions."
  exit 1
fi

# Anonymize and export the last 100 lines of the log file to a file
if [ -f "$log_file" ]; then
  echo "Anonymized log (last 100 lines):"
  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"
else
  echo "Log file $log_file not found."
  exit 1
fi

# Anonymize and export the config file to a file
if [ -f "$config_file" ]; then
  echo -e "\nAnonymized config 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"/' "$config_file" > "$config_output_file"
else
  echo "Config file $config_file not found."
  exit 1
fi

cat $output_file
cat $log_output_file
cat $config_output_file

echo "Basic system info saved to $output_file"
echo "Anonymized log saved to $log_output_file"
echo "Anonymized config saved to $config_output_file"
"""

FANCYDISPLAY = '/var/tmp/pwnagotchi/FancyDisplay.png'

class FancyDisplay:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(FancyDisplay, cls).__new__(cls)
        return cls._instance

    def __init__(self, enabled=False, fps=1, th_path='', mode='screen_saver', sub_mode='show_logo', config={}):
        self.enabled = enabled
        self.image_lock = threading.Lock()
        self.is_image_locked = False
        self.th_path = th_path
        self.displayImpl = None
        self.hijack_frame = None
        self.task = None
        self.loop = None
        self.thread = None
        self.is_running_event = asyncio.Event()
        self.stop_event = threading.Event()
        self.running = False
        self.fps = fps
        self.fb = self.find_fb_device()
        self.current_mode = mode
        self.current_screen_saver = sub_mode
        self.modes = ['screen_saver', 'auxiliary', 'terminal']
        self.screen_saver_modes = ['show_logo', 'moving_shapes', 'random_colors', 'hyper_drive', 'show_animation']
        if config: self.screen_data = config
        else: self.screen_data = {}
        self.set_mode(mode, sub_mode)
        logging.info("[FancyDisplay] FancyDisplay initialized.")

    def _start_loop(self):
        logging.info("[FancyDisplay] Starting the asyncio event loop in a new thread.")
        asyncio.set_event_loop(self.loop)
        self.is_running_event.set()
        try:
            self.loop.run_until_complete(self.screen_controller())
        except asyncio.CancelledError:
            pass
        finally:
            self.loop.close()
            self.is_running_event.clear()

    def start(self, res, rot, col):
        logging.debug("[FancyDisplay] Starting display controller.")
        self._res = res
        self._rot = rot
        self._col = col
        self.displayImpl = self.display_hijack()

        if self.loop is None or self.loop.is_closed():
            self.loop = asyncio.new_event_loop()
            self.thread = threading.Thread(target=self._start_loop, daemon=True)
            self.thread.start()

        while not self.is_running_event.is_set():
            time.sleep(0.1)

    def stop(self):
        self.running = False
        if self.loop and not self.loop.is_closed():
            self.loop.call_soon_threadsafe(self.loop.stop)
        if self.thread:
            self.thread.join()
        self.loop = None
        self.thread = None
        logging.debug("[FancyDisplay] Display controller stopped.")

    async def screen_controller(self):
        self.running = True
        while self.running:
            await self.refacer()
            await asyncio.sleep(0.1)

    def is_running(self):
        if self.is_running_event is not None:
            return self.is_running_event.is_set()
        logging.error("[FancyDisplay] is_running_event is not initialized.")
        return False

    def cleanup(self):
        logging.debug("[FancyDisplay] Cleaning up the FancyDisplay resources.")
        self.task = None
        if self.loop is not None:
            if not self.loop.is_closed():
                logging.debug("[FancyDisplay] Closing event loop.")
                self.loop.close()
        self.loop = None
        self.thread = None
        self.displayImpl = None
        self.hijack_frame = None
        self.screen_data = {}
      
    def _calculate_aspect_ratio(self, width, height, aspect_ratio):
        if width < height:
            new_width = width
            new_height = int(new_width / aspect_ratio)
        else:
            new_height = height
            new_width = int(new_height * aspect_ratio)
        return new_width, new_height

    def screen(self):
        return  self.hijack_frame

    async def refacer(self):
        try: 
            fps = 1 / self.fps 
            refresh_interval = 1
            iteration = 0
            while self.running:
                if iteration % refresh_interval == 0:
                    self.hijack_frame = self.get_mode_image()

                if self.hijack_frame is not None:
                    canvas = self.hijack_frame
                    if self._rot == 90:
                        canvas = canvas.rotate(90, expand=True)
                    elif self._rot == 180:
                        canvas = canvas.rotate(180, expand=True)
                    elif self._rot == 270:
                        canvas = canvas.rotate(270, expand=True)

                    if self.enabled:
                        canvas = canvas.resize((self._res[0], self._res[1])).convert(self._col)
                        self.displayImpl.render(canvas)
                else:
                    logging.warning("[FancyDisplay] No image to display.")
                
                await asyncio.sleep(fps)
                iteration += 1

        except asyncio.CancelledError:
            logging.warning("[FancyDisplay] refacer cancelled.")
    def display_hijack(self):
        try:
            args = argparse.Namespace(
                config='/etc/pwnagotchi/default.toml', 
                user_config='/etc/pwnagotchi/config.toml', 
                do_manual=False, 
                skip_session=False, 
                do_clear=False, 
                debug=False, 
                version=False, 
                print_config=False, 
                wizard=False, 
                check_update=False, 
                donate=False
            )
            config = utils.load_config(args)
            display_type = config['ui']['display']['type']
            display = config['ui']['display']['enabled']
            self.displayImpl = None

            displayImpl = getattr(self, 'displayImpl', None)
            if not displayImpl or not displayImpl.config.get('enabled', False):
                self.displayImpl = display_for(config)
                self.displayImpl.config['rotation'] = 0
                logging.debug(self.displayImpl.config)

                if hasattr(self.displayImpl, 'initialize') or not self.enabled:
                    logging.debug('[Fancygotchi] Initializing display')
                    if self.enabled:
                        self.displayImpl.initialize()
                    self.displayImpl.config['enabled'] = True
                    return self.displayImpl
                else:
                    logging.debug('[Fancygotchi] Failed to initialize display: No initialization method found.')
            else:
                logging.debug('[Fancygotchi] Display is already initialized.')

        except KeyError as e:
            logging.error(f'[FancyDisplay] KeyError while display hijacking: {e}')
            logging.error(traceback.format_exc())
            
    def glitch_text_effect(self, text, glitch_chance=0.2, max_spaces=3):
        lines = text.split('\n')
        glitched_lines = []

        for line in lines:
            if random.random() < glitch_chance: 
                num_spaces = random.randint(1, max_spaces) 
                line = ' ' * num_spaces + line 

            glitched_lines.append(line)

        return '\n'.join(glitched_lines)

    def set_mode(self, mode, sub_mode=None, config={}):
        if mode in self.modes:
            logging.debug(f"[FancyDisplay] Switching to mode: {mode}")
            self.current_mode = mode
            if mode == "screen_saver":
                self.set_screen_saver_mode(sub_mode)
                self.screen_cdata = config
            elif mode == "auxiliary":
                self.screen_data = config
            elif mode == "terminal":
                self.screen_data = config 
        else:
            logging.warning(f"[FancyDisplay] Invalid mode: {mode}. Available modes are: {self.modes}")
    
    def switch_mode(self, direction='next'):
        current_index = self.modes.index(self.current_mode)
        sub_mode = None
        if direction == 'next':
            next_index = (current_index + 1) % len(self.modes)
        elif direction == 'previous':
            next_index = (current_index - 1) % len(self.modes)
        else:
            logging.warning(f"[FancyDisplay] Invalid direction: {direction}. Using 'next' as default.")
            next_index = (current_index + 1) % len(self.modes)
        
        next_mode = self.modes[next_index]
        
        logging.debug(f"[FancyDisplay] Switching to the {direction} mode: {next_mode}")
        if next_mode == "screen_saver": 
            sub_mode = self.current_screen_saver
        self.set_mode(next_mode, sub_mode)
        self.set_screen_saver_mode(sub_mode)
        self.current_mode = next_mode
        return next_mode

    def find_fb_device(self):
        for i in range(10): 
            fb_device = f"/dev/fb{i}"
            if os.path.exists(fb_device):
                return fb_device
        return None

    def get_fb_size(self):
        import subprocess
        output = subprocess.check_output(['fbset', '-s']).decode('utf-8')
        for line in output.split('\n'):
            if 'geometry' in line:
                parts = line.split()
                return int(parts[1]), int(parts[2])
        return self._res[0], self._res[1] 

    def read_fb(self, width, height):
        with open(self.fb, "rb") as fb:
                return memoryview(fb.read(width * height * 2))

    def terminal_mode(self):
        if self.fb is None:
            return self.show_logo()

        fb_width, fb_height = self.get_fb_size()
        fb_data = self.read_fb(fb_width, fb_height)
        
        rgb_image = self.convert_to_rgb(fb_data, fb_width, fb_height)
        image = Image.fromarray(rgb_image, mode='RGB')
        
        width, height = self._res
        resized_image = image.resize((width, height), Image.BILINEAR)
        
        return resized_image

    def convert_to_rgb(self, fb_data, width, height):
        rgb_array = np.zeros((height, width, 3), dtype=np.uint8)
        pixels = np.frombuffer(fb_data, dtype=np.uint16)
        
        r = ((pixels >> 11) & 0x1F) << 3
        g = ((pixels >> 5) & 0x3F) << 2
        b = (pixels & 0x1F) << 3
        
        rgb_array[..., 0] = r.reshape(height, width)
        rgb_array[..., 1] = g.reshape(height, width)
        rgb_array[..., 2] = b.reshape(height, width)
        
        return rgb_array

    def set_screen_saver_mode(self, sub_mode):
        if sub_mode is None:
            sub_mode = self.current_screen_saver
        if sub_mode in self.screen_saver_modes:
            logging.debug(f"[FancyDisplay] Switching screen_saver to: {sub_mode}")
            self.current_screen_saver = sub_mode
            if sub_mode == 'show_logo':
                options = {}
            elif sub_mode == 'moving_shapes':
                options = {
                    "shape_type": "text", 
                    "text": "Fancygotchi", 
                    "font_path": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 
                    "color": "red", 
                    "speed": 10, 
                    "font_size": 15,
                }
            elif sub_mode == 'random_colors':
                options = {
                    "speed": 1,
                }
            elif sub_mode == 'hyper_drive':
                num_stars = 100 
                options = {
                    'stars': [
                        {
                            'position': [random.randint(-self._res[0]//2, self._res[0]//2), random.randint(-self._res[1]//2, self._res[1]//2)],
                            'velocity': random.uniform(2, 5),  
                            'size': random.uniform(1, 3),
                            'streak_length': random.uniform(5, 20),
                            'color': 'white'
                        } for _ in range(num_stars)
                    ],
                    'speed': 1.0 
                }
            elif sub_mode == 'show_animation':
                frames_path = os.path.join(self.th_path, 'img', 'boot') if self.th_path else ''
                options = {
                    'frames_path': frames_path,
                    'max_loops': 1,
                    'total_duration': 10,
                }
            self.screen_data.update(options)
            logging.info(f"[FancyDisplay] Screen saver options: {self.screen_data}")
        else:
            logging.warning(f"[FancyDisplay] Invalid screen_saver sub-mode: {sub_mode}. Available sub-modes are: {self.screen_saver_modes}")

    
    def switch_screen_saver_submode(self, direction='next'):
        if self.current_mode != 'screen_saver':
            logging.warning(f"[FancyDisplay] Not in screen_saver mode. Current mode is: {self.current_mode}")
            return self.current_mode
        
        current_index = self.screen_saver_modes.index(self.current_screen_saver)
        
        if direction == 'next':
            next_index = (current_index + 1) % len(self.screen_saver_modes) 
        elif direction == 'previous':
            next_index = (current_index - 1) % len(self.screen_saver_modes)  
        else:
            logging.error(f"[FancyDisplay] Invalid direction: {direction}. Must be 'next' or 'previous'.")
            return self.current_mode
        
        next_submode = self.screen_saver_modes[next_index]
        logging.warning(f"[FancyDisplay] Switching to the {direction} screen_saver sub-mode: {next_submode}")
        self.set_screen_saver_mode(next_submode)
        return next_submode

    def get_mode_image(self):
        logging.debug(f"[FancyDisplay] Getting mode image: {self.current_mode}")
        if self.current_mode == 'screen_saver':
            return self.get_screen_saver_image()
        elif self.current_mode == 'auxiliary':
            return self.auxiliary_image()
        elif self.current_mode == 'terminal':
            return self.terminal_mode()
        else:
            logging.warning(f"[FancyDisplay] Unknown mode: {self.current_mode}. Falling back to default.")
            return self.show_logo()

    def get_screen_saver_image(self):
        if self.current_screen_saver == 'show_logo':
            return self.show_logo() 
        elif self.current_screen_saver == 'moving_shapes':
            return self.moving_shapes_screen_saver()
        elif self.current_screen_saver == 'random_colors':
            return self.random_colors_screen_saver()
        elif self.current_screen_saver == 'hyper_drive':
            return self.hyperdrive_screen_saver()
        elif self.current_screen_saver == 'show_animation':
            return self.show_animation_screen_saver()
        else:
            logging.warning(f"[FancyDisplay] Unknown screen_saver sub-mode: {self.current_screen_saver}.")
            self.current_screen_saver = 'show_logo'
            return self.show_logo() 


    def auxiliary_image(self):
        image = self.show_logo()
        draw = ImageDraw.Draw(image)
        font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12)
        text = "Auxiliary mode"
        text_color = (255, 0, 0) 
        image_width, image_height = image.size
        try:
            text_width, text_height = draw.textsize(text, font)
        except:
            _, _, text_width, text_height = draw.textbbox((0, 0),text, font)
        position = ((image_width - text_width) // 2, 10)
        draw.text(position, text, font=font, fill=text_color)
        return image

    def show_logo(self):
        try:
            width = self._res[0]
            height = self._res[1]
            canvas = Image.new('RGBA', (width, height), 'black')
            draw = ImageDraw.Draw(canvas)
            font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 3)
            text = self.glitch_text_effect(LOGO, glitch_chance=0.25, max_spaces=5)
            try:
                text_width, text_height = draw.textsize(text, font=font)
            except:
                _, _, text_width, text_height = draw.textbbox((0, 0), text, font=font)
            logo_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0))
            draw_logo = ImageDraw.Draw(logo_img)
            draw_logo.text((0, 0), text, fill='lime', font=font)
            aspect_ratio = text_width / text_height
            new_width, new_height = self._calculate_aspect_ratio(width, height, aspect_ratio)
            resized_logo = logo_img.resize((new_width, new_height))
            x = (width - new_width) // 2
            y = (height - new_height) // 2
            canvas.paste(resized_logo, (x, y), resized_logo)
            self.hijack_frame = canvas
            return canvas
        except KeyError as e:
            logging.debug(f'[FancyDisplay] KeyError while showing logo: {e}')
            logging.debug(traceback.format_exc())

    def moving_shapes_screen_saver(self):
        try:
            font_path = self.screen_data.get('font_path')
            font_size = self.screen_data.get('font_size')
            shape_type = self.screen_data.get('shape_type')
            text = self.screen_data.get('text')
            color = self.screen_data.get('color')
            speed = self.screen_data.get('speed')

            width, height = self._res
            font = ImageFont.truetype(font_path, font_size)

            if shape_type == "text":
                try:
                    shape_width, shape_height = font.getsize(text)
                except:
                    _, _, shape_width, shape_height = font.getbbox(text)
            else:
                shape_width = shape_height = shape_size 
            if not hasattr(self, 'shape_position'):
                self.shape_position = [random.randint(0, width - shape_width), random.randint(0, height - shape_height)]
                self.shape_velocity = [random.choice([-1, 1]) * speed, random.choice([-1, 1]) * speed] 
            x, y = self.shape_position
            vx, vy = self.shape_velocity
            if x + shape_width >= width or x <= 0:
                vx = -vx
            if y + shape_height >= height or y <= 0:
                vy = -vy
            x += vx
            y += vy
            self.shape_position = [x, y]
            self.shape_velocity = [vx, vy]

            canvas = Image.new('RGBA', (width, height), 'black')
            draw = ImageDraw.Draw(canvas)

            if shape_type == "text":
                draw.text((x, y), text, font=font, fill=color)
            else:
                draw.ellipse((x, y, x + shape_width, y + shape_height), fill=color)
            return canvas
        except KeyError as e:
            logging.error(f'[FancyDisplay] KeyError while moving shapes: {e}')
            logging.error(traceback.format_exc())

    def random_colors_screen_saver(self):
        speed = self.screen_data.get('speed')
        width, height = self._res
        canvas = Image.new('RGBA', (width, height), (
            random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), 255))
        time.sleep(speed)
        return canvas

    def hyperdrive_screen_saver(self):
        width, height = self._res
        canvas = Image.new('RGBA', (width, height), 'black')
        draw = ImageDraw.Draw(canvas)
        
        center_x, center_y = width // 2, height // 2
        speed = self.screen_data.get('speed', 1.0)
        
        stars = self.screen_data['stars']
        
        for star in stars:
            pos_x, pos_y = star['position']
            velocity = star['velocity'] * speed 
            size = star['size']
            streak_length = star['streak_length']
            
            pos_x *= (1 + velocity / 100)
            pos_y *= (1 + velocity / 100)
            
            streak_end_x = pos_x * (1 + streak_length / 100)
            streak_end_y = pos_y * (1 + streak_length / 100)

            size = min(size * (1 + velocity / 10), 10)
            
            draw.line([(center_x + streak_end_x, center_y + streak_end_y), 
                    (center_x + pos_x, center_y + pos_y)], fill=star['color'], width=int(size))
            
            if abs(pos_x) > width // 2 or abs(pos_y) > height // 2:
                star['position'] = [random.randint(-50, 50), random.randint(-50, 50)]
                star['velocity'] = random.uniform(2, 5)
                star['size'] = random.uniform(1, 3)
                star['streak_length'] = random.uniform(5, 20)
                
                pos_x, pos_y = star['position']
                velocity = star['velocity'] * speed
                pos_x *= (1 + velocity / 100)
                pos_y *= (1 + velocity / 100)
                streak_end_x = pos_x * (1 + star['streak_length'] / 100)
                streak_end_y = pos_y * (1 + star['streak_length'] / 100)
                
                draw.line([(center_x + streak_end_x, center_y + streak_end_y), 
                        (center_x + pos_x, center_y + pos_y)], fill=star['color'], width=int(star['size']))

            star['position'] = [pos_x, pos_y]
        
        return canvas

    def show_animation_screen_saver(self):
        try:
            if self.screen_data is None:
                logging.error("[FancyDisplay] screen_data is None. Unable to show animation screen saver.")
                return self.show_logo() 
                
            frames_path = self.screen_data.get('frames_path', '')
            max_loops = self.screen_data.get('max_loops', 1)
            total_duration = self.screen_data.get('total_duration', 10)
            target_fps = 24
            frame_duration = 0.2

            if not frames_path or not os.path.exists(frames_path):
                image = self.show_logo()
                return image

            valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif')
            frames = sorted([f for f in os.listdir(frames_path) if f.lower().endswith(valid_extensions)])
            
            if not frames:
                logging.error("[FancyDisplay] No valid frames found in the specified directory")
                return None

            if not hasattr(self, 'animation_state'):
                self.animation_state = {
                    'start_time': time.time(),
                    'loop_count': 0,
                    'extracted_frames': []
                }

            current_time = time.time()
            elapsed_time = current_time - self.animation_state['start_time']

            if (self.animation_state['loop_count'] >= max_loops):
                self.animation_state['start_time'] = current_time
                self.animation_state['loop_count'] = 0
                self.animation_state['extracted_frames'] = []

            if not self.animation_state['extracted_frames']:
                for frame in frames:
                    frame_path = os.path.join(frames_path, frame)
                    if frame.lower().endswith('.gif'):
                        with Image.open(frame_path) as img:
                            for gif_frame in ImageSequence.Iterator(img):
                                self.animation_state['extracted_frames'].append(copy.deepcopy(gif_frame))
                    else:
                        self.animation_state['extracted_frames'].append(Image.open(frame_path))
                
                logging.debug(f"[FancyDisplay] Extracted {len(self.animation_state['extracted_frames'])} frames")

            total_frames = len(self.animation_state['extracted_frames'])
            current_frame_index = int((elapsed_time / frame_duration) % total_frames)

            current_frame = self.animation_state['extracted_frames'][current_frame_index]

            image = current_frame.resize((self._res[0], self._res[1])).convert(self._col)

            if current_frame_index == 0 and elapsed_time > 0: 
                self.animation_state['loop_count'] += 1

            if image is None:
                image = self.show_logo()
            return image

        except Exception as ex:
            logging.error(f"[FancyDisplay] Error in show_animation_screen_saver: {ex}")
            logging.error(traceback.format_exc())
            return None

class FancyMenu:
    def __init__(self, fancygotchi, menu_theme, custom_menus={}):
        
        self._fancygotchi = fancygotchi
        self.menus = copy.deepcopy(MENUS)
        self.scroll_state = {}
        self.menu_theme = menu_theme
        self.menu_stack = [self.menus['Main menu']]
        self.active = False
        self.timeout = menu_theme['timeout']

        self.last_activity_time = time.time()
        self.plugin_names = get_all_plugin_names(self._fancygotchi)
        self.populate_plugins_menu(self.plugin_names)
        self.populate_themes_menu()
        if custom_menus != {}:
            self.load_menu_config(custom_menus)

        self.reset_menus(custom_menus)

    def reset_menus(self, custom_menus={}):
        self.menus = copy.deepcopy(MENUS) 
        self.menu_stack = [self.menus['Main menu']]
        self.populate_plugins_menu(self.plugin_names)
        self.populate_themes_menu()
        if custom_menus:
            self.load_menu_config(custom_menus)

    def load_menu_config(self, config):
        menus = {}
        main = {}
        issues = []
        for menu_key, menu_data in config.items():
            if not isinstance(menu_data, dict):
                issues.append(f"[FancyMenu] Menu data for '{menu_key}' is not a dictionary.")
                continue
            menu_title = menu_data.get("options", {}).get("title", menu_key)
            action = {"action": "submenu", "name": menu_title}
            if not menu_contains_button(self.menu_stack[0], menu_title):
                self.menu_stack[0].add_button(menu_title, action)
            menu_title = menu_data.get("options", {}).get("title", menu_key)
            back_menu = menu_data.get("options", {}).get("back", "Main menu") or "Main menu"
            buttons = []
            for btn_key, btn_data in menu_data.items():
                if btn_key.startswith("btn"):
                    if not isinstance(btn_data, dict):
                        issues.append(f"[FancyMenu] Button data for '{btn_key}' in menu '{menu_key}' is not a dictionary.")
                        continue
                    title = btn_data.get("title", f"Button {btn_key[-1]}")
                    buttons.append((title, btn_data))
            menus[menu_title] = Menu(menu_title, buttons, back_reference=back_menu)
            self.menus.update(menus)
        if issues:
            logging.warning("[FancyMenu] Issues encountered during menu configuration: \n" + "\n".join(issues))

    def populate_plugins_menu(self,plugin_names):
        menus = {}
        sorted_plugin_names = sorted(plugin_names)
        menus['Plugins toggle'] = Menu('Plugins toggle', [], back_reference='Plugins')
        for plugin in sorted_plugin_names:
            if plugin.lower() != 'fancygotchi':
                if plugin != 'None' and plugin is not None:
                    menus[plugin] = Menu(plugin, [
                        ("Enable plugin", {"action": "plugin", "name": plugin, "enable": True}),
                        ("Disable plugin", {"action": "plugin", "name": plugin, "enable": False}),
                    ], back_reference='Plugins toggle')
                    menus['Plugins toggle'].items.append((
                        plugin.capitalize(), {"action": "submenu", "name": plugin}
                    ), )
        self.menus.update(menus)

    def populate_themes_menu(self):
        theme_names = self._fancygotchi.theme_list()
        menus = {}
        sorted_theme_names = sorted(theme_names)
        menus['Theme selector'] = Menu('Theme selector', [], back_reference='Fancygotchi')
        for theme in theme_names:
            menus[theme] = Menu(theme, [
                (f"{theme} 0", {"action": "theme_select", "name": theme, "rotation": 0,}),
                (f"{theme} 90", {"action": "theme_select", "name": theme, "rotation": 90}),
                (f"{theme} 180", {"action": "theme_select", "name": theme, "rotation": 180}),
                (f"{theme} 270", {"action": "theme_select", "name": theme, "rotation": 270}),
            ], back_reference='Theme selector')
            menus['Theme selector'].items.append((
                theme.capitalize(), {"action": "submenu", "name": theme}
            ), )
        self.menus.update(menus)

    def toggle(self):
        self.active = not self.active
        self.last_activity_time = time.time() 
        return self.active

    def navigate(self, direction):
        if self.active:
            current_menu = self.menu_stack[-1]
            if direction in ['up', 'down']:
                current_menu.navigate(direction)
            elif direction == 'left':
                if len(self.menu_stack) > 1:
                    self.menu_stack.pop()
            elif direction == 'right':
                selected_item = current_menu.items[current_menu.current_index]
                if isinstance(selected_item[1], dict) and selected_item[1].get('action') == 'submenu':
                    submenu_name = selected_item[1]['name']
                    if submenu_name in self.menus:
                        self.menu_stack.append(self.menus[submenu_name])
            self.last_activity_time = time.time()  
    def select(self):
        current_menu = self.menu_stack[-1]
        return current_menu.items[current_menu.current_index][1]

    def check_timeout(self):
        if self.timeout != 0:
            current_time = time.time()
            if current_time - self.last_activity_time > self.timeout:
                logging.debug("[FancyMenu] Session timed out.")
                self.active = False
                return True
            return False
        else:
            self.active = True
            return False

    def render(self):
        try:
            if self.active:
                if self.check_timeout():
                    return

                if not hasattr(self, 'loaded_images'):
                    self.loaded_images = {}

                current_menu = self.menu_stack[-1]
                rot = self._fancygotchi._config['main']['plugins']['Fancygotchi']['rotation']
                if rot == 0 or rot == 180:
                    canvas_width, canvas_height = self._fancygotchi._res
                elif rot == 90 or rot == 270:
                    canvas_width = self._fancygotchi._res[1]
                    canvas_height = self._fancygotchi._res[0]

                menu_width = self.menu_theme.get('width', 100)
                menu_height = self.menu_theme.get('height', '100%')

                menu_x, menu_y, menu_x2, menu_y2 = Fancygotchi.pos_convert(
                    self._fancygotchi,
                    self.menu_theme.get('position', [0, 0])[0],
                    self.menu_theme.get('position', [0, 0])[1],
                    menu_width,
                    menu_height,
                    r=0,
                    r0=canvas_width,
                    r1=canvas_height,
                )

                if self.menu_theme.get('bg_color', (0, 0, 0, 0)) == '': bg_color = (0,0,0,0)
                else: bg_color = self.menu_theme.get('bg_color', (0, 0, 0, 0))
                text_speed = self.menu_theme.get('motion_text_speed', 20)
                menu_width = menu_x2 - menu_x
                menu_height = menu_y2 - menu_y
   
                menu_image = Image.new("RGBA", (menu_width, menu_height), bg_color)
                draw = ImageDraw.Draw(menu_image)

                draw.rectangle([0, 0, menu_width, menu_height], fill=bg_color)

                bg_image_path = None
                if self.menu_theme.get('bg_image', None):
                    bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('bg_image'))
                if bg_image_path:
                    if bg_image_path not in self.loaded_images:
                        if os.path.exists(bg_image_path):
                            try:
                                bg_image = Image.open(bg_image_path)
                                self.loaded_images[bg_image_path] = bg_image
                            except Exception as e:
                                logging.warning(f"Failed to load background image: {e}")
                                self.loaded_images[bg_image_path] = None 
                        else:
                            logging.warning(f"Background image not found: {bg_image_path}")
                            self.loaded_images[bg_image_path] = None
                            
                    if self.loaded_images[bg_image_path]:
                        bg_mode = self.menu_theme.get('bg_mode', 'normal')
                        bg_tmp = image_mode(menu_image, self.loaded_images[bg_image_path], bg_mode)

                title_font_size = self.menu_theme.get('title_font_size', 'Medium')
                title_font = getattr(self._fancygotchi, title_font_size)
                title_color = self.menu_theme.get('title_color', 'black')

                if title_font:
                    title_text = current_menu.name
                    try:
                        title_width, title_height = draw.textsize(title_text, font=title_font)
                    except:
                        _, _, title_width, title_height = draw.textbbox((0,0),title_text, font=title_font)

                    title_x, title_y, _, _ = Fancygotchi.pos_convert(
                        self._fancygotchi,
                        self.menu_theme.get('title_position', ['center', '5'])[0],
                        self.menu_theme.get('title_position', ['center', '5'])[1],
                        title_width,
                        title_height,
                        r=0,
                        r0=menu_width,
                        r1=menu_height,
                    )
                    try:
                        title_box = draw.textsize(title_text, font=title_font)
                        title_size = (title_box[0], title_box[1])
                    except:
                        title_box = draw.textbbox((0, 0), title_text, font=title_font)
                        title_size = (title_box[2], title_box[3])
                    if title_size[0] > menu_width and self.menu_theme.get('motion_text', True):
                        self.scroll_text(draw, title_text, title_color, title_text, title_font, menu_width, text_speed)
                    else:
                        draw.text((title_x, title_y), title_text, font=title_font, fill=title_color)

                btn_height = self.menu_theme.get('button_height', 15)
                btns_width = self.menu_theme.get('buttons_width', '90%')
                btns_height = self.menu_theme.get('buttons_height', '90%')
                button_spacing = self.menu_theme.get('button_spacing', 5)

                if isinstance(btns_width, str) and '%' in btns_width:
                    base_width = menu_width
                    btns_menu_width = int((base_width / 100) * int(btns_width.replace('%', '')))
                else:
                    btns_menu_width = int(btns_width)

                if isinstance(btns_height, str) and '%' in btns_height:
                    base_height = (menu_height - title_height - title_y)
                    btns_menu_height = int((base_height / 100) * int(btns_height.replace('%', '')))
                else:
                    btns_menu_height = int(btns_height)

                buttons_x, buttons_y, buttons_x1, buttons_y1 = Fancygotchi.pos_convert(
                    self._fancygotchi,
                    self.menu_theme.get('buttons_position', ['center', 'center'])[0],
                    self.menu_theme.get('buttons_position', ['center', 'center'])[1],
                    btns_width,
                    btns_height,
                    r=0,
                    r0=menu_width,
                    r1=menu_height,
                )

                button_font_size = self.menu_theme.get('button_font_size', 'Medium')
                button_font = getattr(self._fancygotchi, button_font_size, None)

                visible_buttons = (menu_height - title_height - title_y) // (btn_height + button_spacing)
                scroll_offset = max(0, current_menu.current_index - visible_buttons + 1)

                for i, (item_name, item_action) in enumerate(current_menu.items[scroll_offset:scroll_offset + visible_buttons]):
                    button_y = title_height + title_y + i * (btn_height + button_spacing)

                    if button_font:
                        button_text = item_name
                        try:
                            text_width, text_height = draw.textsize(button_text, font=button_font)
                        except:
                            _, _, text_width, text_height = draw.textbbox((0, 0), button_text, font=button_font)
                        

                    text_x, text_y, _, _ = Fancygotchi.pos_convert(
                        self._fancygotchi,
                        self.menu_theme.get('text_position', ['center', '5'])[0],
                        self.menu_theme.get('text_position', ['center', '5'])[1],
                        text_width,
                        text_height,
                        r=0,
                        r0=btns_menu_width,
                        r1=btn_height,
                    )

                    button_image = Image.new("RGBA", (btns_menu_width, btn_height), bg_color)
                    
                    button_draw = ImageDraw.Draw(button_image)
                    if self.menu_theme.get('button_bg_color', (0,0,0,0)) == '': button_bg_color = (0,0,0,0)
                    else: button_bg_color = self.menu_theme.get('button_bg_color', 'white')

                    button_bg_image_path = None
                    highlight_button_bg_image_path = None

                    if self.menu_theme.get('button_bg_image', ''):
                        button_bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('button_bg_image'))

                    if self.menu_theme.get('highlight_button_bg_image', ''):
                        highlight_button_bg_image_path = os.path.join(self._fancygotchi._th_path, 'img', 'menu', self.menu_theme.get('highlight_button_bg_image'))

                    if button_bg_image_path and button_bg_image_path not in self.loaded_images:
                        if os.path.exists(button_bg_image_path):
                            try:
                                button_bg_image = Image.open(button_bg_image_path)
                                button_bg_image = button_bg_image.convert("RGBA")
                                try:
                                    button_bg_image = button_bg_image.resize((btns_menu_width, btn_height), Image.ANTIALIAS)
                                except:
                                    button_bg_image = button_bg_image.resize((btns_menu_width, btn_height), Image.Resampling.LANCZOS)
                                self.loaded_images[button_bg_image_path] = button_bg_image
                            except Exception as e:
                                logging.error(f"[FancyMenu] Failed to load button background image: {e}")
                                self.loaded_images[button_bg_image_path] = None
                        else:
                            logging.warning(f"Button background image not found: {button_bg_image_path}")
                            self.loaded_images[button_bg_image_path] = None

                    if highlight_button_bg_image_path and highlight_button_bg_image_path not in self.loaded_images:
                        if os.path.exists(highlight_button_bg_image_path):
                            try:
                                highlight_button_bg_image = Image.open(highlight_button_bg_image_path)
                                highlight_button_bg_image = highlight_button_bg_image.convert("RGBA")
                                try:
                                    highlight_button_bg_image = highlight_button_bg_image.resize((btns_menu_width, btn_height), Image.ANTIALIAS)
                                except:
                                    highlight_button_bg_image = highlight_button_bg_image.resize((btns_menu_width, btn_height), Image.Resampling.LANCZOS)
                                self.loaded_images[highlight_button_bg_image_path] = highlight_button_bg_image
                            except Exception as e:
                                logging.error(f"[FancyMenu] Failed to load highlight button background image: {e}")
                                self.loaded_images[highlight_button_bg_image_path] = None
                        else:
                            logging.warning(f"Highlight button background image not found: {highlight_button_bg_image_path}")
                            self.loaded_images[highlight_button_bg_image_path] = None

                    if i + scroll_offset == current_menu.current_index:
                        highlight_color = self.menu_theme.get('highlight_color', 'black')
                        highlight_text_color = self.menu_theme.get('highlight_text_color', 'white')
                        
                        button_draw.rectangle([0, 0, btns_menu_width, btn_height], fill=highlight_color)

                        image_to_use_path = highlight_button_bg_image_path if self.loaded_images.get(highlight_button_bg_image_path) else button_bg_image_path
                        if self.loaded_images.get(image_to_use_path):
                            button_image.paste(self.loaded_images[image_to_use_path], (0, 0), self.loaded_images[image_to_use_path].split()[3])

                        try:
                            button_box = button_draw.textsize(button_text, font=button_font)
                            button_size = (button_box[0], button_box[1])
                        except:
                            button_box = button_draw.textbbox((0, 0), button_text, font=button_font)
                            button_size = (button_box[2], button_box[3])
                        if button_size[0] > menu_width and self.menu_theme.get('motion_text', True):
                            self.scroll_text(button_draw, button_text, highlight_text_color, button_text, button_font, menu_width, text_speed)
                        else:
                            button_draw.text((text_x, text_y), button_text, font=button_font, fill=highlight_text_color)

                    else:
                        button_text_color = self.menu_theme.get('button_text_color', 'black')
                        button_draw.rectangle([0, 0, btns_menu_width, btn_height], fill=button_bg_color)

                        if self.loaded_images.get(button_bg_image_path):
                            button_image.paste(self.loaded_images[button_bg_image_path], (0, 0), self.loaded_images[button_bg_image_path].split()[3])

                        try:
                            button_box = button_draw.textsize(button_text, font=button_font)
                            button_size = (button_box[0], button_box[1])
                        except:
                            button_box = button_draw.textbbox((0, 0), button_text, font=button_font)
                            button_size = (button_box[2], button_box[3])
                        if button_size[0] > menu_width and self.menu_theme.get('motion_text', True):
                            self.scroll_text(button_draw, button_text, button_text_color, button_text, button_font, menu_width, text_speed)
                        else:
                            button_draw.text((text_x, text_y), button_text, font=button_font, fill=button_text_color)
                    menu_image.paste(button_image, (buttons_x, button_y), button_image.split()[3])

                    draw.rectangle([0, 0, menu_width - 1, menu_height - 1], outline=self.menu_theme.get('border_color', 'black'))

                    canvas = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0))
                    canvas.paste(menu_image, (menu_x, menu_y))

                return canvas

        except Exception as e:
            logging.error(f"Failed to render menu: {e}")
            logging.error(traceback.format_exc())

    def scroll_text(self, draw, menu_item_key, color, scrolltext, scrollfont, menu_width, distance=10):
        scroll_state = self.scroll_state.get(menu_item_key, None)
        if not scroll_state:
            try:
                text_width, text_height = draw.textsize(scrolltext, font=scrollfont)
            except:
                _, _, text_width, text_height = draw.textbbox((0, 0), scrolltext, font=scrollfont)
            
            scroll_state = {
                'text_width': text_width,
                'position': 10 
            }
            self.scroll_state[menu_item_key] = scroll_state

        text_width = scroll_state['text_width']
        text_position = scroll_state['position']
        draw.text((text_position, 0), scrolltext, font=scrollfont, fill=color)

        if text_position + text_width < menu_width:
            draw.text((text_position + text_width, 0), f' - {scrolltext}', font=scrollfont, fill=color)

        text_position -= distance

        if text_position + text_width <= 0:
            text_position += text_width

        self.scroll_state[menu_item_key]['position'] = text_position

class Menu:
    def __init__(self, name, items, back_reference="Main menu"):
        self.name = name
        self.back_reference = back_reference
        self.current_index = 0

        if not name == 'Main menu':
            if self.back_reference == "Main menu":
                self.items = [
                    ("Home", {"action": "submenu", "name": "Main menu"})
                ] + items
            else:
                self.items = [
                    ("Back", {"action": "submenu", "name": back_reference}),
                    ("Home", {"action": "submenu", "name": "Main menu"})
                ] + items
        else:
            self.items = items

    def navigate(self, direction):
        if direction in ['up', 'down']:
            self.current_index = (self.current_index + (1 if direction == 'down' else -1)) % len(self.items)

    def add_button(self, title, action):
        self.items.insert(0, (title, action))  

def menu_contains_button(menu, button_name):
    for item in menu.items:
        if item[0] == button_name:
            return True
    return False

MENUS = {
    'Main menu': Menu('Main menu', [
        ("Plugins", {"action": "submenu", "name": "Plugins"}),
        ("Fancygotchi",{"action": "submenu", "name": "Fancygotchi"}),
        ("System", {"action": "submenu", "name": "System"}),
    ]),
    'System': Menu('System', [
        ("Restart Auto", {"action": "restart", "mode": "auto"}),
        ("Restart Manu", {"action": "restart", "mode": "manu"}),
        ("Reboot Auto", {"action": "reboot", "mode": "auto"}),
        ("Reboot Manu", {"action": "reboot", "mode": "manu"}),
        ("Shutdown", {"action": "shutdown"}),
    ]),
    'Fancygotchi': Menu('Fancygotchi', [
        ("Theme selector", {"action": "submenu", "name": "Theme selector"}),
        ("Second screen", {"action": "submenu", "name": "Second screen"}),
        ("Theme refresh", {"action": "theme_refresh"}),
        ("Stealth mode", {"action": "stealth_mode"}),
    ]),
    'Plugins': Menu('Plugins', [
        ("Refresh plugins", {"action": "refresh_plugins"}),
        ("Plugins toggle", {"action": "submenu", "name": "Plugins toggle"}),
    ]),
    'Second screen': Menu('Second screen', [
        ('Activate second screen', {'action': 'enable_second_screen'}),
        ('Switch screen mode', {'action': 'switch_screen_mode'}),
        ('Switch screen saver mode', {'action': 'switch_screen_saver'}),
    ]),
}

def check_internet_and_repo():
    try:
        requests.get("https://www.google.com", timeout=5)
        response = requests.get(THEMES_REPO, timeout=5)
        
        if response.status_code == 200:
            return True, "Connection successful"
        else:
            error_msg = f"Repository not accessible. Status code: {response.status_code}"
            logging.warning(error_msg)
            return False, error_msg
            
    except requests.ConnectionError as e:
        error_msg = f"No internet connection: {str(e)}"
        logging.warning(error_msg)
        return False, error_msg
        
    except requests.Timeout as e:
        error_msg = f"Connection timed out: {str(e)}"
        logging.warning(error_msg)
        return False, error_msg

def get_all_plugin_names(fancygotchi):
    config_dict = fancygotchi._config 
    plugins = list(config_dict['main'].get('plugins', {}).keys())
    custom_plugins_path = config_dict['main'].get('custom_plugins', '')
    all_plugins = plugins
    return all_plugins

def is_int(s):
    try:
        int(s)
        return True
    except ValueError:
        return False

def box_to_xywh(position):
    dist_1 = math.sqrt(position[0]**2 + position[1]**2)
    dist_2 = math.sqrt(position[2]**2 + position[3]**2)
    
    if dist_1 <= dist_2:
        x, y = position[0], position[1]
        x2, y2 = position[2], position[3]
    else:
        x, y = position[2], position[3]
        x2, y2 = position[0], position[1]

    w = abs(x - x2)
    h = abs(y - y2)
    
    return [x, y, w, h]

def adjust_image(image_path, zoom, mask=False, refine=150, alpha=False, invert=False, crop=[0,0,0,0]):
    try:
        if isinstance(image_path, str):
            try:
                image = Image.open(image_path)
            except Exception as e:
                logging.error(f"Error opening image: {e}")
                return None
        elif isinstance(image_path, Image.Image):
            image = image_path
        if invert:
            image = invert_pixels(image)
        if crop != [0,0,0,0]:
            image = image.crop(crop)
        image = image.convert('RGBA') 
        
        original_width, original_height = image.size
        new_width = int(original_width * zoom)
        new_height = int(original_height * zoom)

        adjusted_image = image.resize((new_width, new_height))
        if mask:
            new_img = adjusted_image
            adjusted_image = masking(new_img, refine)
        
        if alpha: 
            adjusted_image = alphamask(adjusted_image)
        
        return adjusted_image
    except Exception as e:
        logging.error("Error:", str(e))
        return None
    
def invert_pixels(image):
    try:
        image = image.convert('RGBA')
        data = list(image.getdata())
        inverted_data = [(255-r, 255-g, 255-b, a) for r, g, b, a in data]
        inverted_image = Image.new('RGBA', image.size)
        inverted_image.putdata(inverted_data)
        return inverted_image
    except Exception as e:
        logging.error(f"Error in invert_pixels: {str(e)}")
        logging.error(traceback.format_exc())
        return image  

def alphamask(src_image):
    src_image = src_image.convert('RGBA')
    data = src_image.getdata()
    newData = []
    for item in data:
        if item[0] in range(240, 256) and item[1] in range(240, 256) and item[2] in range(240, 256):
            newData.append((255, 255, 255, 0))
        else:
            newData.append(item)
    src_image.putdata(newData)
    src_image = src_image.convert('RGBA')
    return src_image

def masking(src_image, refine):
    image = src_image.convert('RGBA') 
    width, height = image.size
    pixels = image.getdata()
    new_pixels = []
    for pixel in pixels:
        r, g, b, a = pixel
        if a > refine:
            new_pixel = (0, 0, 0, 255)
        else:
            new_pixel = (0, 0, 0, 0)
        new_pixels.append(new_pixel)
    new_img = Image.new("RGBA", image.size)
    new_img.putdata(new_pixels)
    adjusted_image = new_img
    return adjusted_image

def image_mode(canvas, image, mode):
    w, h = canvas.size
    width, height = image.size
    logging.debug(f"Mode: {mode}")
    logging.debug(f"Image size: {width}x{height}")
    logging.debug(f"Canvas size: {w}x{h}")
    if mode == 'normal':
        image = image.convert('RGBA')
        canvas.paste(image, (0,0,width, height), image.split()[3])
    elif mode == 'stretch':
        img_resized = image.resize((w,h))
        canvas.paste(img_resized, (0, 0), img_resized)
    elif mode == 'tile':
        for x in range(0, w, image.width):
            for y in range(0, h, image.height):
                canvas.paste(image, (x, y), image)
    elif mode == 'center':
        x = (w - image.width) // 2
        y = (h - image.height) // 2
        canvas.paste(image, (x, y), image)
    elif mode == 'fit':
        original_width, original_height = image.size
        canvas_width, canvas_height = canvas.size

        original_aspect = original_width / original_height
        canvas_aspect = canvas_width / canvas_height

        if original_aspect > canvas_aspect:
            new_width = canvas_width
            new_height = int(canvas_width / original_aspect)
        else:
            new_height = canvas_height
            new_width = int(canvas_height * original_aspect)

        try:
            image_resized = image.resize((new_width, new_height), Image.ANTIALIAS)
        except:
            image_resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)

        x = (canvas_width - new_width) // 2
        y = (canvas_height - new_height) // 2

        canvas.paste(image_resized, (x, y), image_resized) 

    elif mode == 'fill':
        img_resized = ImageOps.fit(image, (w,h))
        canvas.paste(img_resized, (0, 0), img_resized)
    return canvas

def verify_font_info(ft):
    font_list = [fonts.Bold, fonts.BoldSmall, fonts.Medium, fonts.Huge, fonts.BoldBig, fonts.Small]

    font_info = {
        'Bold': fonts.Bold,
        'BoldSmall': fonts.BoldSmall,
        'Medium': fonts.Medium,
        'Huge': fonts.Huge,
        'BoldBig': fonts.BoldBig,
        'Small': fonts.Small
    }

    for font in font_info:
        if font_info[font].size == ft.size and font_info[font].getname() == ft.getname():
            return font
    return ft

def allowed_file(filename):
    allowed_ext = {'zip'}
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_ext

def unzip_file(zip_file, extract_to):
    with zipfile.ZipFile(zip_file, 'r') as zip_ref:
        zip_ref.extractall(extract_to)
    os.remove(zip_file)

def serializer(obj):
    if isinstance(obj, set):
        return list(obj)
    raise TypeError

def _compile_po_to_mo(po_file_path):
    """
    Compiles a .po file to a .mo file in memory.
    This is a lightweight, pure-Python implementation based on the standard
    `msgfmt.py` tool.
    """
    try:
        with open(po_file_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()

        messages = {}
        msgid = ""
        msgstr = ""
        is_fuzzy = False
        in_msgid = False
        in_msgstr = False

        for line in lines:
            line = line.strip()
            if not line:
                if msgid and not is_fuzzy:
                    messages[msgid] = msgstr
                msgid, msgstr, is_fuzzy = "", "", False
                in_msgid, in_msgstr = False, False
            elif line.startswith('#,') and 'fuzzy' in line:
                is_fuzzy = True
            elif line.startswith('msgid '):
                in_msgid, in_msgstr = True, False
                msgid = line[6:].strip('"')
            elif line.startswith('msgstr '):
                in_msgid, in_msgstr = False, True
                msgstr = line[7:].strip('"')
            elif line.startswith('"'):
                if in_msgid:
                    msgid += line.strip('"')
                elif in_msgstr:
                    msgstr += line.strip('"')

        if msgid and not is_fuzzy:
            messages[msgid] = msgstr

        # Build the .mo file format in memory
        magic = 0x950412de
        revision = 0
        num_strings = len(messages)
        
        # Sort by msgid
        sorted_messages = sorted(messages.items())

        # Create string tables
        orig_table = b''
        trans_table = b''
        for msgid, msgstr in sorted_messages:
            orig_table += msgid.encode('utf-8') + b'\0'
            trans_table += msgstr.encode('utf-8') + b'\0'

        # Calculate offsets
        header_size = 7 * 4
        orig_offset_table_offset = header_size
        trans_offset_table_offset = orig_offset_table_offset + num_strings * 8
        strings_offset = trans_offset_table_offset + num_strings * 8

        output = bytearray(struct.pack('<IIIIIII', magic, revision, num_strings, orig_offset_table_offset, trans_offset_table_offset, 0, 0))
        
        orig_addr = strings_offset
        trans_addr = strings_offset + len(orig_table)
        for msgid, msgstr in sorted_messages:
            output.extend(struct.pack('<II', len(msgid), orig_addr))
            orig_addr += len(msgid) + 1
        for msgid, msgstr in sorted_messages:
            output.extend(struct.pack('<II', len(msgstr), trans_addr))
            trans_addr += len(msgstr) + 1

        output.extend(orig_table)
        output.extend(trans_table)
        return bytes(output)

    except Exception as e:
        logging.error(f"[Fancygotchi] Error compiling .po to .mo: {e}", exc_info=True)
        return None

class Fancygotchi(plugins.Plugin):
    __author__ = 'V0rT3x'
    __github__ = 'https://github.com/V0r-T3x/Fancygotchi'
    __version__ = '2.0.8'
    __license__ = 'GPL3'
    __description__ = 'The Ultimate theme manager for pwnagotchi'

    def __init__(self):
        self.pyenv = sys.executable    
        self.running = False
        self.fancy_menu = None
        self.actions_log = []
        self.second_screen = Image.new('RGBA', (1,1), 'black')
        self.fancy_menu_img = None
        self.display_config = {'mode': 'screen_saver', 'sub_mode': 'show_logo'}
        self.screen_modes = ['screen_saver', 'auxiliary', 'terminal']
        self.screen_saver_modes = ['show_logo', 'moving_shapes', 'random_colors', 'hyper_drive', 'show_animation']
        self.dispHijack = False
        self.loop = None
        self.refacer_thread = None
        self._stop_event = threading.Event()

        self.bitmap_widget = ('Bitmap', 'WardriverIcon', 'InetIcon', 'Frame', 'WifiQR')
        self._config = pwnagotchi.config
        self.gittoken = self._config['main']['plugins']['Fancygotchi'].get('github_token', None)
        self.cfg_path = None
        self.cursor_list = ['█', '-']
        self.options = dict()
        self._agent = None
        self.ready = False
        self.stealth_mode = False
        self.refresh = False
        self.refresh_menu = False
        self.last_cmd = None
        self.refresh_trigger = -1
        self.star = '*'
        logging.info(f'[Fancygotchi]{20*self.star}[Fancygotchi]{20*self.star}')
        self._pwny_root = os.path.dirname(pwnagotchi.__file__)
        self._plug_root = os.path.dirname(os.path.realpath(__file__))
        self.orientation = 'horizontal'
        self._default = {
            'theme': {
                'options': {
                    'boot_animation': False,
                    'boot_mode': 'normal', # Implementation to adjust the boot animation image with normal, stretch, fit, fill, center or tile
                    'boot_max_loops': 1,
                    'boot_total_duration': 1,
                    'screen_mode': 'screen_saver',
                    'screen_saver': 'show_logo',
                    'second_screen_fps': 1,
                    'webui_fps': 1,
                    'second_screen_webui': True,
                    'bg_fg_select': 'manu',
                    'bg_mode': 'normal',
                    'fg_mode': 'normal',
                    'fg_image': '',
                    'bg_color': 'white',
                    'bg_image': '',
                    'bg_anim_image': '',
                    #[Bold, BoldSmall, Medium, Huge, BoldBig, Small]
                    'font_sizes': [14, 9, 14, 25, 19, 9],
                    'font': 'DejaVuSansMono',
                    'font_bold': 'DejaVuSansMono-Bold',
                    'status_font': 'DejaVuSansMono',
                    'font_awesome': '',
                    'size_offset': 5,
                    'label_spacing': 9,
                    'label_line_spacing': 0,
                    'cursor': '❤',
                    'friend_bars': '▌',
                    'friend_no_bars': '│',
                    'base_text_color': ['black'],
                    'main_text_color': ['black'],
                    'color_mode': ['P', 'P'],
                    'faces': {
                        'look_r': "( ⚆_⚆)",
                        'look_l': "(☉_☉ )",
                        'look_r_happy': "( ◕‿◕)",
                        'look_l_happy': "(◕‿◕ )",
                        'sleep': "(⇀‿‿↼)",
                        'sleep2': "(≖‿‿≖)",
                        'awake': "(◕‿‿◕)",
                        'bored': "(-__-)",
                        'intense': "(°▃▃°)",
                        'cool': "(⌐■_■)",
                        'happy': "(•‿‿•)",
                        'excited': "(ᵔ◡◡ᵔ)",
                        'grateful': "(^‿‿^)",
                        'motivated': "(☼‿‿☼)",
                        'demotivated': "(≖__≖)",
                        'smart': "(✜‿‿✜)",
                        'lonely': "(ب__ب)",
                        'sad': "(╥☁╥ )",
                        'angry': "(-_-')",
                        'friend': "(♥‿‿♥)",
                        'broken': "(☓‿‿☓)",
                        'debug': "(#__#)",
                        'upload': "(1__0)",
                        'upload1': "(1__1)",
                        'upload2': "(0__1)",
                    }
                },
                'widget': {}
            }
        }
        self._default_menu = {
            'motion_text': True,
            'motion_text_speed': 20,

            'bg_select': 'manu',
            'bg_mode': 'normal',

            'position': [0, 0],
            'title_position': ['center', '5'],
            'title_font_size': 'Medium',
            'title_color': 'black',
            'width': 100,
            'height': '100%',

            'buttons_position': ['center', '5'],
            'buttons_width': '90%',
            'button_height': 15,
            'button_spacing': 3,

            'bg_color': 'white',
            'border_color': 'black',
            'highlight_color': 'black',
            'highlight_border_color': 'white',
            'highlight_text_color': 'white',
            'button_bg_color': 'white',
            'button_bg_border_color': 'black',
            'button_text_color': 'black',

            'bg_image': "",
            'button_bg_image': "",
            'highlight_button_bg_image': "",

            'text_position': ['center', 'center'],
            'button_font_size': "Medium",
            'timeout': 30,
        }
        text_widget_defaults = {
            'position': [0, 0],
            'color': ['#000000'],
            'z_axis': 0,
            'text_font': '',
            'text_font_size': "Medium",
            'size_offset': 0,
            'icon': False,
            'icon_color': False,
            'invert': False,
            'alpha': False,
            'crop': [0,0,0,0],
            'mask': False,
            'refine': 150,
            'zoom': 1,
            'image_type': 'png',
            'wrap': False,
            'max_length': 0
        }
        labeledvalue_widget_defaults = {
            'position': [0, 0],
            'color': ['#000000'],
            'z_axis': 0,
            'text_font': '',
            'text_font_size': "Medium",
            'size_offset': 0,
            'icon': False,
            'icon_color': False,
            'invert': False,
            'alpha': False,
            'crop': [0,0,0,0],
            'mask': False,
            'refine': 150,
            'zoom': 1,
            'label': '',
            'label_font': '',
            'label_font_size': "Medium",
            'label_spacing': 0,
            'label_line_spacing': 0,
            'f_awesome': False,
            'f_awesome_size': 0
        }
        line_widget_defaults = {
            'position': [0, 0, 0, 0],
            'color': ['#000000'],
            'z_axis': 0,
            'width': 1
        }
        rect_widget_defaults = {
            'position': [0, 0, 0, 0],
            'color': ['#000000'],
            'z_axis': 0
        }
        filledrect_widget_defaults = {
            'position': [0, 0, 0, 0],
            'color': ['#000000'],
            'z_axis': 0
        }
        bitmap_widget_defaults = {
            'position': [0, 0],
            'color': ['#000000'],
            'z_axis': 0,
            'icon': False,
            'invert': False,
            'alpha': False,
            'crop': [0,0,0,0],
            'mask': False,
            'refine': 150,
            'zoom': 1,
            'icon_color': False,
        }
        self.widget_defaults = {
            'Text': text_widget_defaults,
            'LabeledValue': labeledvalue_widget_defaults,
            'Line': line_widget_defaults,
            'Rect': rect_widget_defaults,
            'FilledRect': filledrect_widget_defaults,
            'Bitmap': bitmap_widget_defaults
        }
        
        self._theme_name = 'Default'
        self._theme = copy.deepcopy(self._default)
        self._th_path = None
        self._res = []
        self._color_mode = ['P', 'P']
        self._bg = ''
        self._fg = ''
        self._i = 0
        self._imax = None
        self._frames = []
        self._icolor = 0
        self.font_name = 'DejaVuSansMono'
        self.font_bold_name = 'DejaVuSansMono-Bold'
        self.f_awesome_name = ''
        self.Bold = None
        self.BoldSmall = None
        self.BoldBig = None
        self.Medium = None
        self.Small = None
        self.Huge = None
        self._state = {}
        self._state_default = {}

        self.Tag = '# Pwned by V0rT3x'
        v_code = [{'replace': False, 
                    'reference': 'lv.draw(self._canvas, drawer)',
                    'paste': """
                # Start of the Fancygotchi hack
                if hasattr(self, '_pwncanvas'):
                    rot = pwnagotchi.config['main']['plugins']['Fancygotchi']['rotation']
                    if self._pwncanvas_tmp is not None:
                        self._pwncanvas = self._pwncanvas_tmp
                        self._pwncanvas_tmp = None
                    if self._pwncanvas is not None:
                        if isinstance(self._pwncanvas, Image.Image):
                            self._canvas = self._canvas.convert('RGBA')
                            self._canvas.paste(self._pwncanvas, (0, 0), self._pwncanvas)
                    web_tmp = self._canvas
                    hw_tmp = self._canvas
                    if rot in [90,270]: hw_tmp = hw_tmp.rotate(-90, expand=True)
                    self._canvas = hw_tmp.convert(self._web_mode)# End of the Fancygotchi hack"""},
                    {'replace': False, 'reference': 'web.update_frame(self._canvas)',
                    'paste':"""                # Start of the Fancygotchi hack
                if hasattr(self, '_pwncanvas'):
                    self._canvas = hw_tmp.convert(self._hw_mode)
                    if rot == 90: self._canvas = self._canvas.rotate(90, expand=True)
                    if rot == 270: self._canvas = self._canvas.rotate(-90, expand=True)
                    if rot == 180: self._canvas = self._canvas.rotate(180) # End of the Fancygotchi hack"""}]
        s_code = [{'replace': False, 
                    'reference': 'self._listeners[key](prev, value)',
                    'paste': """
    # Start of the Fancygotchi hack                    
    def get_attr(self, key, attribute='value'):
        with self._lock:
            if key in self._state:
                return getattr(self._state[key], attribute)
            else:
                return None
    # End of the Fancygotchi hack
    """}]
        p_code  = [{'replace': False, 
                'reference': 'source /usr/bin/pwnlib',
                'paste': f"""
# Start of the Fancygotchi hack
if [ -f "/usr/local/bin/boot_animation.py" ]; then
    {self.pyenv} /usr/local/bin/boot_animation.py
fi # End of the Fancygotchi hack"""}]
        v_f = os.path.join(self._pwny_root, 'ui', 'view.py')
        s_f = os.path.join(self._pwny_root, 'ui', 'state.py')
        p_f = '/usr/bin/pwnagotchi-launcher'
        rst = 0
        if self.adjust_code(v_f, v_code): rst = 1
        if self.adjust_code(s_f, s_code): rst = 1
        if self.adjust_code(p_f, p_code): rst = 1
        if self.zram_check(): rst = 1
        if self.fps_check(): rst = 1
        self.check_and_fix_fb()
        if rst:
            self.log('The pwnagotchi need to restart.')
            os.system('sudo systemctl restart pwnagotchi.service')
            os.system('sudo service pwnagotchi restart')
        self.log('Initiated')

    def adjust_code(self, file_path, changes):
        self.log(f'Adjusting code in {file_path}')
        rst = 0
        with open(file_path, 'r') as file:
            lines = file.readlines()

        for code in changes:
            replace_flag = code.get('replace', False)
            reference_lines = code.get('reference', '').split('\n')
            paste_code = code.get('paste', '')

            if not lines[-1].strip() == self.Tag:
                reference_index = 0
                for i, line in enumerate(lines):
                    if reference_index < len(reference_lines) and reference_lines[reference_index] in line:
                        reference_index += 1
                    else:
                        reference_index = 0
                    if reference_index == len(reference_lines):
                        if replace_flag:
                            lines[i - len(reference_lines) + 1:i + 1] = [paste_code + '\n']
                        else:
                            lines[i] = lines[i].rstrip() + '\n' + paste_code + '\n'
                        rst = 1
        if rst:
            lines.append(self.Tag + '\n')
        with open(file_path, 'w') as file:
            file.writelines(lines)
        
        return rst

    def check_and_fix_fb(self):
        config_paths = [
            "/boot/firmware/config.txt",
            "/boot/config.txt"
        ]
        correct_overlay = "dtoverlay=vc4-fkms-v3d"
        wrong_overlay = "dtoverlay=vc4-kms-v3d"

        fb_device_exists = any(os.path.exists(f"/dev/fb{i}") for i in range(10))
        self.log(f"Framebuffer device exists: {fb_device_exists}")
        config_file = None
        for path in config_paths:
            if os.path.exists(path):
                config_file = path
                break

        if not config_file:
            return

        with open(config_file, 'r') as file:
            lines = file.readlines()

        found_correct_overlay = any(correct_overlay in line for line in lines)

        if fb_device_exists:
            self.log("Framebuffer device exists. No reboot needed.")
            return
        elif found_correct_overlay:
            self.log("config.txt already contains the correct overlay. No reboot needed.")
            return
        else:
            self.log("Framebuffer device does not exist config.txt already don't contain the correct overlay. Rebooting system to apply changes...")

        backup_path = config_file + ".bak"
        shutil.copy(config_file, backup_path)
        with open(config_file, 'r') as file:
            lines = file.readlines()
        found_wrong_overlay = False
        found_correct_overlay = False
        new_lines = []
        for line in lines:
            if wrong_overlay in line:
                found_wrong_overlay = True
                new_lines.append(line.replace(wrong_overlay, correct_overlay))
            elif correct_overlay in line:
                found_correct_overlay = True
                new_lines.append(line)
            else:
                new_lines.append(line)
        if not found_correct_overlay:
            new_lines.append(f"\n{correct_overlay}\n")
            self.log(f"{correct_overlay} added to {config_file}")
        with open(config_file, 'w') as file:
            file.writelines(new_lines)
        self.log("Rebooting system to apply changes...")
        subprocess.run(["sudo", "reboot"])

    def zram_check(self):
        rst = 0
        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']:
            fs_data = self._config['fs']['memory']['mounts']['data']
            if 'enabled' in fs_data and fs_data['enabled']:
                if 'mount' != '': mount = fs_data['mount']
                else: self._config['fs']['memory']['mounts']['data']['mount'] = "/var/tmp/pwnagotchi"
                if 'zram' in fs_data and fs_data['zram']:
                    if 'size' in fs_data:
                        size = num_size = int(re.search(r'\d+', fs_data['size']).group())
                        if num_size < 50:
                            self._config['fs']['memory']['mounts']['data']['size'] = '250M' 
                            save_config(self._config, '/etc/pwnagotchi/config.toml')
                            rst= 1
        return rst

    def fps_check(self):
        rst = 0
        if 'ui' in self._config and 'fps' in self._config['ui']:
            fps_value = int(self._config['ui']['fps'])
            if fps_value == 0:
                self._config['ui']['fps'] = 1
                save_config(self._config, '/etc/pwnagotchi/config.toml')
                rst = 1
        return rst

    def log(self, msg):
        try:
            # working state
            # log = False
            # debug = True
            log = False
            debug = True

            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'log' in self._theme['theme']['dev']:
                log = self._theme['theme']['dev']['log']

            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'debug' in self._theme['theme']['dev']:
                debug = self._theme['theme']['dev']['debug']

            if log:
                if debug: logging.debug(msg)
                else: logging.info(f'[Fancygotchi] {msg}')
        except Exception as ex:
            logging.error(ex)

    def on_ready(self, agent):
        self._agent = agent
        self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO'

    def on_loaded(self):
        logging.info("[Fancygotchi] Loaded")
        self.ready = True

    def on_unload(self, ui):
        with open('/etc/pwnagotchi/config.toml', 'r') as f:
            f_toml = toml.load(f)
            faces.load_from_config(f_toml['ui']['faces'])
        with ui._lock:
            self.cleanup_display()
            self.dispHijack = False
            if not self.dispHijack:
                if hasattr(self, 'display_controller') and self.display_controller:
                    self.display_controller.stop()
                if hasattr(ui, '_enabled') and not ui._enabled:
                    ui._enabled = True
                    self.log("Switched back to the original display.")
        if self._config['ui']['display']['enabled']:
            ui._enabled = True
            ui.init_display()
            # Start of Fancygotchi unload voice modification
            try:
                locale_path = os.path.join(self._pwny_root, 'locale', 'fancyvoice')
                if os.path.islink(locale_path):
                    os.unlink(locale_path)
                    self.log("Removed fancyvoice symlink.")
                
                # Reload the voice with the original system language
                if hasattr(ui, '_config'):
                    original_lang = ui._config['main']['lang']
                    self.reload_voice(ui, lang=original_lang)
            except Exception as e:
                logging.error(f"[Fancygotchi] Error during unload cleanup: {e}")
            # End of Fancygotchi unload voice modification

            self.cleanup_display()
        if hasattr(self, 'fancy_menu'):
            del self.fancy_menu
        if hasattr(self, 'listener'):
            self.listener.close()
        if hasattr(ui, '_pwncanvas'):
            # Start of Fancygotchi unload voice modification
            del ui._pwncanvas
        if hasattr(ui, '_pwncanvas_tmp'):
            del ui._pwncanvas_tmp
        if hasattr(ui, '_update'):
            del ui._update
        if hasattr(ui, '_web_mode'):
            del ui._web_mode
        if hasattr(ui, '_hw_mode'):
            del ui._hw_mode
        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/screenshots')
        if os.path.exists(screenshots_path):
            os.system(f'rm -r {screenshots_path}')
        repo_screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')
        if os.path.exists(repo_screenshots_path):
            os.system(f'rm -r {repo_screenshots_path}')
        css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')
        css_backup = css_dst + '.backup'
        if os.path.exists(css_backup):
            copyfile(css_backup, css_dst)
            os.remove(css_backup)
        img_dst = os.path.join(self._pwny_root, 'ui/web/static/img')
        if os.path.islink(img_dst):
            os.unlink(img_dst)
        font_dst = '/usr/share/fonts/truetype/theme_fonts'
        os.system('rm %s' % (font_dst))
        icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')
        icon_bkup = icon_dst + '.backup'
        if os.path.exists(icon_bkup):
            copyfile(icon_bkup, icon_dst)
            os.remove(icon_bkup)
        fancytools_path = "/usr/local/bin/fancytools"
        if os.path.exists(fancytools_path):
            os.remove(fancytools_path)
        diagnostic_path = "/usr/local/bin/diagnostic.sh"
        if os.path.exists(diagnostic_path):
            os.remove(diagnostic_path)

        logging.info('[Fancygotchi] Unloaded')

    def on_ui_setup(self, ui):
        logging.info('[Fancygotchi] UI setup start')
        setattr(ui, '_pwncanvas_tmp', None)
        setattr(ui, '_pwncanvas', None)
        setattr(ui, '_web_mode', self._color_mode[0])
        setattr(ui, '_hw_mode', self._color_mode[1])
        setattr(ui, '_update', {
            'update': True,
            'partial': False,
            'dict_part': {}
        })
        self.log(f"UI attributes created: {ui._update}, {ui._web_mode}, {ui._hw_mode}")
        self._res = [ui._width, ui._height]
        self.log(f"UI resolution: {self._res}")
        self.theme_update(ui, True)
        self.pwncanvas_creation(self._res)
        self.fps = 1
        if self._th_path is None: self._th_path = ''
        self.log(f"FPS: {self.fps}")
        self.log(f"Theme path: {self._th_path}")
        self.log(self._config['ui']['display']['enabled'])
        self.display_controller = FancyDisplay(self._config['ui']['display']['enabled'], self.fps, self._th_path, )
        self.log('UI setup finished')

    def cleanup_display(self):

        if hasattr(self, 'display_controller') and self.display_controller:
            if self.display_controller.is_running():
                self.display_controller.stop()
            self.display_controller = None
            del self.display_controller

    def _share_state(self, ui):
        """Shares a read-only copy of the internal state with the ui object."""
        if not hasattr(ui, 'fancy'):
            # Create a simple namespace object on ui if it doesn't exist
            ui.fancy = type('fancy', (object,), {})()
        
        # Provide a deep copy to prevent other plugins from modifying the internal state
        ui.fancy._state = copy.deepcopy(self._state)
        logging.debug("[Fancygotchi] Shared internal state with ui.fancy._state")


    def button_controller(self, cmd=None, screen=1):
        screen = int(screen)
        logging.warning(f"Screen {screen} controlled")
        logging.warning(f"cmd: {cmd}")
        try:
            # verify if the button command is valid
            if cmd:
                button_command = cmd
            else:
                return
            # if linked to the first screen
            if button_command:
                cmd = button_command['action']
                if screen == 1:
                    logging.warning("Screen 1 controlled")
                    if cmd == 'btn_start':
                        logging.warning("Button start")
                        self.fancy_menu.toggle()
                        self.log('button start')
                    # Verifying if menu exists and is enabled
                    if hasattr(self, 'fancy_menu') and self.fancy_menu.active:
                        logging.warning("Fancy menu is active")
                        self.log(f'button_command: {cmd}')
                        
                        if cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:
                            direction = cmd.split('_')[1]
                            self.fancy_menu.navigate(direction)
                            self.log(direction)
                        elif cmd == 'btn_select':
                            cmd_action = self.fancy_menu.select()
                            logging.warning(f"cmd_action: {cmd_action}")
                            self.last_cmd = cmd_action
                else:
                    logging.warning("Screen 2 controlled")
        except Exception as e:
            logging.error(f"Error in button_controller: {e}")
            logging.error(traceback.format_exc())

    def navigate_fancymenu(self, cmd=None):
        try:
            if cmd:
                menu_command = cmd
            else:
                return
            if hasattr(self, 'fancy_menu'):
                if menu_command:
                    cmd = menu_command['action']
                    self.log(f'menu_command: {cmd}')
                    if cmd == 'btn_start':
                        self.fancy_menu.toggle()
                        self.log('start button')
                    elif cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:
                        direction = cmd.split('_')[1]
                        self.fancy_menu.navigate(direction)
                        self.log(direction)
                    elif cmd == 'btn_select':
                        cmd_action = self.fancy_menu.select()
                        logging.warning(f"cmd_action: {cmd_action}")
                        self.last_cmd = cmd_action
                        #im here
                        self.log('select')
        except Exception as e:
            logging.error(f"Error in navigate_fancymenu: {e}")
            logging.error(traceback.format_exc())

    def on_ui_update(self, ui):
        try:            
            if self.dispHijack:
                if not (hasattr(self, 'display_controller') and self.display_controller and self.display_controller.is_running()):
                    logging.debug("[Fancygotchi] Starting display hijack.")
                    self.display_controller = FancyDisplay(self._config['ui']['display']['enabled'], self.fps, self._th_path)
                    self.display_controller.start(self._res, self.options.get('rotation', 0), self._color_mode[1])
                    mode, submode, config = self.display_config.get('mode', 'screen_saver'), self.display_config.get('sub_mode', 'show_logo'), self.display_config.get('config', {})
                    self.display_controller.set_mode(mode, submode, config)
                if hasattr(ui, '_enabled') and ui._enabled:
                    ui._enabled = False
            #elif not self.dispHijack:
            elif not self.dispHijack and self._config['ui']['display']['enabled']:
                if hasattr(self, 'display_controller') and self.display_controller and self.display_controller.is_running():
                    self.display_controller.stop()
                #if hasattr(ui, '_enabled') and not ui._enabled and self._config['ui']['display']['enabled'] and not ui.is_rebooting():
                if hasattr(ui, '_enabled') and not ui._enabled:
                    ui._enabled = True
                    ui.init_display()


            # Check for theme updates
            if (hasattr(ui, '_update') and ui._update.get('update')) or self.refresh:
                is_partial = hasattr(ui, '_update') and ui._update.get('partial', False)
                self.log(f"Theme update triggered. Partial: {is_partial}, Refresh: {self.refresh}")
                
                 # Always process the update, regardless of the theme.
                self.theme_update(ui)
                
                # Crucially, always reset the flags after processing to prevent loops.
                if hasattr(ui, '_update'):
                    ui._update['update'] = False
                    #ui._update['partial'] = False
                    ui._update['partial'] = False # Reset partial flag
                    ui._update['dict_part'] = {}
                    self.log("UI update flags reset.")
                self.refresh = False
               

            self._res = [ui._width, ui._height]
            self.second_screen = Image.new('RGBA', self._res, 'black')
            
            
            th = self._theme['theme']
            self._share_state(ui)
            th_opt = th['options']
            th_widget = th['widget']
            rot = self.options['rotation']
            self.pwncanvas_creation(self._res)
            self.remove_widgets(ui)
            ui_state = list(ui._state.items())
            for key, state in ui_state:
                widget_type = type(state).__name__
                if widget_type in self.bitmap_widget:
                    widget_type = 'Bitmap'
                self.add_widget(ui, key, widget_type, th_widget)
                if  widget_type == 'Text' or widget_type == 'LabeledValue':
                    if not 'value' in self._state[key]:
                        self._state[key].update({'value': None})
                    self._state[key]['value'] = ui._state.get(key)
                    
                    if key == 'name':
                        custom_char = th_opt["cursor"]

                        name_value = ui._state.get(key)

                        for char in self.cursor_list:
                            if name_value.endswith(char):
                                name_value = name_value.rstrip(char) + f' {custom_char}'
                                break 

                        self._state[key]['value'] = name_value

                    if key == 'friend_name' and ui._state.get(key) != None:
                        friend_name = ui._state.get(key)
                        friend_name = friend_name.replace('▌', th_opt['friend_bars']).replace('│', th_opt['friend_no_bars'])
                    
                    value = self._state[key]['value']
                if widget_type == 'Bitmap':
                    
                    if key in th_widget and th_widget[key].get('icon'):
                        img_ref = ui._state.get_attr(key, 'image') 
                        if key in self._state and 'image_dict' in self._state[key]:
                            img_map = self._state[key].get('image_dict')
                            matched_custom_image = None
                            for id_number, (img_a, img_b) in img_map.items():
                                try:
                                    if ImageChops.difference(img_a, img_ref).getsize() is None:
                                        matched_custom_image = img_b
                                        break 
                                except:
                                    if ImageChops.difference(img_a, img_ref).getbbox() is None:
                                        matched_custom_image = img_b
                                        break  
                            if matched_custom_image:
                                self._state[key].update({'image': matched_custom_image})
                            else:
                                self.log('No matching image found.')
                    else:
                        if 'image_dict' not in self._state[key]:
                            self._state[key]['image_dict'] = {}

                        image_dict = self._state[key]['image_dict']
                        original_img = ui._state.get_attr(key, 'image')

                        corresponding_adj_img = None

                        for i, (orig, adj) in image_dict.items():
                            try:
                                if orig == original_img:
                                    corresponding_adj_img = adj
                                    break
                            except AttributeError:
                                continue

                        if corresponding_adj_img is None:
                            i = len(image_dict)
                            try:
                                corresponding_adj_img = adjust_image(original_img, self._state[key]['zoom'], False, self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'])
                                image_dict[i] = [original_img, corresponding_adj_img]
                            except AttributeError:
                                corresponding_adj_img = original_img

                        self._state[key].update({'image_dict': image_dict})

                        self._state[key].update({'image': corresponding_adj_img})

            if 'theme' in self._theme and 'dev' in self._theme['theme'] and 'refresh' in self._theme['theme']['dev']:
                self.refresh_trigger = self._theme['theme']['dev']['refresh']

            if hasattr(self, 'fancy_menu'):
                menu_command = self.last_cmd
                if menu_command:
                    cmd = menu_command['action']
                    if self.dispHijack:
                        if cmd == 'btn_start':
                            self.dispHijack = False
                        elif self.display_config['mode'] == 'screen_saver':
                            if cmd == 'btn_up':
                                self.log('switch screen saver mode')
                                self.process_actions({'action': 'next_screen_saver'})
                            elif cmd == 'btn_down':
                                self.log('switch screen saver mode')
                                self.process_actions({'action': 'previous_screen_saver'})
                            else:
                                self.process_actions(menu_command)
                        elif self.display_config['mode'] == 'auxiliary':
                            self.log('enable auxiliary mode')
                            self.process_actions(menu_command)
                        elif self.display_config['mode'] == 'terminal':
                            self.process_actions(menu_command)
                        else:
                            self.process_actions(menu_command)
                        
                    elif self.fancy_menu.active:
                        self.log(f'menu_command: {menu_command}')
                        self.log(f'menu_command: {cmd}')
                        if cmd == 'btn_start':
                            self.process_actions(menu_command)
                        elif cmd in ['btn_up', 'btn_down', 'btn_left', 'btn_right']:
                            direction = cmd.split('_')[1]
                            self.fancy_menu.navigate(direction)
                            self.log(direction)
                        elif cmd == 'btn_select':
                            menu_cmd = self.fancy_menu.select()
                            self.log(f'menu command:{menu_cmd}')
                            try:
                                self.process_actions(menu_cmd)
                            except OSError as e:
                                logging.error(f'error while processing command: {e}')
                        else:
                            self.process_actions(menu_command)
                    else:
                        self.process_actions(menu_command)
                self.last_cmd = None

            if self._i == self.refresh_trigger:
                self.theme_update(ui)

            self.drawer()

            if rot == 90 or rot == 270:
                self._pwncanvas = self._pwncanvas.rotate(90, expand=True)

            if hasattr(ui, '_pwncanvas_tmp') and ui._pwncanvas_tmp == None:
                setattr(ui, '_pwncanvas_tmp', self._pwncanvas)
            if hasattr(ui, '_pwncanvas') and ui._pwncanvas == None:
                setattr(ui, '_pwncanvas', self._pwncanvas)

            if self._imax != None:
                if self._imax - 1 == self._i:
                    self._i = 0
                else:
                    self._i += 1

        except Exception as e:
            self.log("non fatal error while updating Fancygotchi: %s" % e)
            self.log(traceback.format_exc())
    
    # Theme section
    def generate_default_config(self, config_path, actual_state):
        default_config = {
            'theme': {
                'options': copy.deepcopy(self._default['theme']['options']),
                'menu': {
                    'options': copy.deepcopy(self._default_menu),
                },
                'widget': {}
            }
        }

        for widget_name, state in actual_state.items():
            widget_type = state['widget_type']
            if widget_type in self.bitmap_widget:
                widget_type = 'Bitmap'
            default_widget_config = self.widget_defaults.get(widget_type, {})
            
            default_config['theme']['widget'][widget_name] = copy.deepcopy(default_widget_config)

        for widget_name, state in self._state_default.items():
            if widget_name in default_config['theme']['widget']:
                default_config['theme']['widget'][widget_name].update(state)

                if 'widget_type' in default_config['theme']['widget'][widget_name]:
                    del default_config['theme']['widget'][widget_name]['widget_type']

        with open(config_path, 'w') as f:
            toml.dump(default_config, f)

        logging.debug(f"Default configuration saved to {config_path}")
        return default_config

    def refresh_plugins(self):
        new_plugs = ''
        if 'custom_plugins' in self._agent._config['main']:
            path = self._agent._config['main']['custom_plugins']
            logging.debug("loading plugins from %s" % (path))
            for filename in glob.glob(os.path.join(path, "*.py")):
                plugin_name = os.path.basename(filename.replace(".py", ""))
                if not plugin_name in plugins.database:
                    logging.debug("New plugin: %s" % (plugin_name))
                    plugins.database[plugin_name] = filename
                    new_plugs += ",%s" % plugin_name
            if new_plugs != '':
                self.log("found new:%s" % (new_plugs))

    def load_and_run_module(self, module_path):
        if module_path.startswith('/'): module_file_path = module_path
        else: module_file_path = os.path.join(self._th_path, 'scripts', module_path)
        
        if not os.path.exists(module_file_path):
            self.log(f"Module file {module_file_path} does not exist.")
            return
        
        try:
            module_name = os.path.splitext(os.path.basename(module_file_path))[0] 
            spec = importlib.util.spec_from_file_location(module_name, module_file_path)
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            
            if hasattr(module, 'main'): 
                module.main()
            else:
                self.log(f"Module {module_name} imported successfully, but no 'main' function found.")
        
        except Exception as e:
            logging.error(f"Error while loading and executing module {module_file_path}: {e}")
            logging.error(traceback.format_exc())

    def process_actions(self, command):
        if command is None:
            logging.error("[Fancygotchi] Action is None, unable to process.")
            return
        try:
            action = command.get('action')
            mode = command.get('mode', 'manu')
            self.actions_log.append(action)
            self.actions_log = self.actions_log[-12:]
            self.log(f'Action: {action}')

            if action == 'submenu':
                self.fancy_menu.navigate("right")
            elif action == 'btn_start':
                self.fancy_menu.toggle()
            elif action == 'plugin':
                # http://10.0.0.2:8080/plugins/Fancygotchi/plugin?name=bt-tether&enable=False
                name = command.get('name')
                state = command.get('enable')

                if name and name != 'None':  # Validate the plugin name
                    self.log(f'Plugin command received: {name}, state: {state}')

                    # Convert state to a boolean if it's provided as a string
                    enable_state = state.lower() == 'true' if isinstance(state, str) else bool(state)

                    # Attempt to toggle the plugin
                    try:
                        is_change = toggle_plugin(name, enable=enable_state)
                        self.log(f"Plugin '{name}' {'changed state' if is_change else 'did not change state'} to {'enabled' if enable_state else 'disabled'}.")
                    except Exception as e:
                        self.log(f"Error toggling plugin '{name}': {e}")
                else:
                    self.log("Invalid plugin name provided for menu_plugin action.")

            elif action == 'refresh_plugins':
                self.refresh_plugins()
            elif action == 'shutdown':
                pwnagotchi.shutdown()
            elif action == 'restart':
                pwnagotchi.restart(mode)
            elif action == 'reboot':
                pwnagotchi.reboot(mode)
            elif action == 'theme_select':
                name = command.get('name')
                rotation = command.get('rotation')
                self.theme_save_config(name, rotation)
                self.refresh = True
            elif action == 'theme_refresh':
                self.refresh = True
            elif action == 'stealth_mode':
                self.stealth_mode = not self.stealth_mode
                self.refresh = True
            elif action == 'switch_screen_mode':
                try:
                    self.display_config['mode'] = self.display_controller.switch_mode()
                except:
                    self.display_config['mode'] = self.screen_modes[(self.screen_modes.index(self.display_config['mode']) + 1) % len(self.screen_modes)]
            elif action == 'switch_screen_mode_reverse':
                try:
                    self.display_config['mode'] = self.display_controller.switch_mode('previous')
                except:
                    self.display_config['mode'] = self.screen_modes[(self.screen_modes.index(self.display_config['mode']) - 1) % len(self.screen_modes)]
            elif action == 'enable_second_screen':
                self.dispHijack = True
                self.fancy_menu.active = False
            elif action == 'disable_second_screen':
                self.log('disable second screen')
                self.dispHijack = False
            elif action == 'next_screen_saver':
                self.log('next screen saver')
                try:
                    self.display_config['sub_mode'] = self.display_controller.switch_screen_saver_submode('next')
                except:
                    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)]
            elif action == 'previous_screen_saver':
                self.log('previous screen saver')
                try:
                    self.display_config['sub_mode'] =  self.display_controller.switch_screen_saver_submode('previous')
                except:
                    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)]
            elif action == 'run_bash':
                script = command.get('file')
                if script.startswith('/'): script_path = script
                else: script_path = os.path.join(self._th_path, 'scripts', script)
                if os.path.exists(script_path):
                    self.log(f'Running script: {script_path}')
                    os.system(f'chmod +x {script_path}')
                    self.log(f'Running command: {script_path}')
                    exit_code = os.system(f'{script_path}')
                    self.log(f"Script exited with code: {exit_code}")
                else:
                    self.log(f"Script not found: {script_path}")
            elif action == 'run_python':
                file_path = command.get('file')
                self.load_and_run_module(file_path)

        except Exception as e:
            logging.error(f'error while processing menu command: {e}')

    def theme_creator(self, theme_name, state, oriented=False, resolution=False):
        themes_folder = os.path.join(self._plug_root, 'themes')
        res = ''

        new_theme_folder = os.path.join(themes_folder, theme_name)
        
        if os.path.exists(new_theme_folder):
            self.log(f"Theme '{theme_name}' already exists. Skipping creation.")
            return False

        os.makedirs(new_theme_folder, exist_ok=True)

        folders = ['config', 'img', 'fonts']
        for folder in folders:
            os.makedirs(os.path.join(new_theme_folder, folder), exist_ok=True)

        img_subfolders = ['bg', 'face', 'friend_face', 'widgets', 'icons']
        for subfolder in img_subfolders:
            os.makedirs(os.path.join(new_theme_folder, 'img', subfolder), exist_ok=True)

        if resolution:
            res = f'{self._res[0]}x{self._res[1]}'
            os.makedirs(os.path.join(new_theme_folder, 'config', res), exist_ok=True)

        info_json = {
            "author": "",
            "version": "1.0.0",
            "resolutions": "",
            "display": "",
            "plugins": ["", ""],
            "notes": ""
        }
        with open(os.path.join(new_theme_folder, 'info.json'), 'w') as f:
            json.dump(info_json, f, indent=2)

        original_css_backup = os.path.join(self._pwny_root, 'ui/web/static/css/style.css.backup')
        original_css_path = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')
        new_css_path = os.path.join(new_theme_folder, 'style.css')

        with open(new_css_path, 'w+') as f:
            f.write(CSS)

        config_path = os.path.join(new_theme_folder, 'config')
        if resolution:
            config_path_res = os.path.join(config_path, res)
            if oriented:
                config_path = os.path.join(config_path_res, 'config-h.toml')
                self.generate_default_config(config_path, state)
                config_path = os.path.join(config_path_res, 'config-v.toml')
                self.generate_default_config(config_path, state)
            else:
                config_path = os.path.join(config_path_res, 'config.toml')
                self.generate_default_config(config_path, state)
        else:
            if oriented:
                config_path_uni = config_path
                config_path = os.path.join(config_path_uni, 'config-h.toml')
                self.generate_default_config(config_path, state)
                config_path = os.path.join(config_path_uni, 'config-v.toml')
                self.generate_default_config(config_path, state)
            else:
                config_path = os.path.join(config_path, 'config.toml')
                self.generate_default_config(config_path, state)

        return True

    def theme_selector(self, config, boot=False):
        self._theme = {} 
        th_path = None
        self._theme_name = 'Default'
        try:
            if not boot: self.log('Theme selector')
            fancy_opt = config['main']['plugins']['Fancygotchi']
            self.options['rotation'] = fancy_opt.get('rotation', 0)

            self._theme = copy.deepcopy(self._default)
            size = f'{self._res[0]}x{self._res[1]}'
            if 'theme' in fancy_opt and fancy_opt['theme'] != '':
                theme = fancy_opt['theme']
                self._theme_name = theme
                rot = fancy_opt['rotation']
                th_path = os.path.join(self._plug_root, 'themes', theme)
                self._th_path = th_path

                cfg_path = os.path.join(th_path, "config")

                if not os.path.exists(cfg_path):
                    self.log(f"Warning: Theme config folder {cfg_path} does not exist, loading default theme.")
                    self._theme = copy.deepcopy(self._default)
                    return

                toml_files = [f for f in os.listdir(cfg_path) if f.endswith('.toml')]
                
                if len(toml_files) == 1:
                    cfg_file = toml_files[0]
                elif 'config-v.toml' in toml_files and 'config-h.toml' in toml_files:
                    cfg_file = 'config-v.toml' if rot in [90, 270] else 'config-h.toml'
                else:
                    size_folder = os.path.join(cfg_path, size)
                    if os.path.exists(size_folder):
                        size_toml_files = [f for f in os.listdir(size_folder) if f.endswith('.toml')]
                        if len(size_toml_files) == 1:
                            cfg_file = os.path.join(size, size_toml_files[0])
                        else:
                            cfg_file = os.path.join(size, 'config-v.toml' if rot in [90, 270] else 'config-h.toml')
                    else:
                        cfg_file = 'config-h.toml'

                self.cfg_path = os.path.join(cfg_path, cfg_file)

                if os.path.exists(self.cfg_path):
                    with open(self.cfg_path, 'r') as f:
                        self._theme = toml.load(f)
                else:
                    self._theme = copy.deepcopy(self._default)

            if th_path:
                css_src = os.path.join(th_path, 'style.css')
                css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')
                css_backup = css_dst + '.backup'
                if os.path.exists(css_src):
                    if not os.path.exists(css_backup):
                        copyfile(css_dst, css_backup)
                    copyfile(css_src, css_dst)

                img_src = os.path.join(th_path, 'img')
                img_dst = os.path.join(self._pwny_root, 'ui/web/static')
                icon_src = os.path.join(th_path, 'img', 'icons', 'favicon.png')
                icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')
                icon_bkup = icon_dst + '.backup'
                icon_dst_dir = os.path.dirname(icon_dst)
                if not os.path.exists(icon_dst_dir):
                    os.makedirs(icon_dst_dir)

                if os.path.exists(icon_src):
                    if not os.path.exists(icon_bkup):
                        if not os.path.exists(icon_dst):
                            copyfile(icon_src, icon_dst)
                        else:
                            copyfile(icon_dst, icon_bkup)
                    copyfile(icon_src, icon_dst)
                else:
                    if os.path.exists(icon_bkup):
                        copyfile(icon_bkup, icon_dst)
                        os.remove(icon_bkup)
                if os.path.exists(img_dst):
                    os.system('rm %s/img' % (img_dst))
                if os.path.exists(img_src):
                    os.system('ln -s %s %s' % (img_src, img_dst))
            else:
                icon_dst = os.path.join(self._pwny_root, 'ui/web/static/images/pwnagotchi.png')
                icon_bkup = icon_dst + '.backup'
                css_dst = os.path.join(self._pwny_root, 'ui/web/static/css/style.css')
                css_backup = css_dst + '.backup'
                if os.path.exists(css_backup):
                    copyfile(css_backup, css_dst)
                    os.remove(css_backup)
                if os.path.exists(icon_bkup):
                    copyfile(icon_bkup, icon_dst)
                    os.remove(icon_bkup)

            if self._theme['theme']['options'].get('faces'):
                self._config['ui']['faces'] = self._theme['theme']['options']['faces']
            faces.load_from_config(self._config['ui']['faces'])

            if not boot:self.log(f'Theme: {self._theme_name}')

        except Exception as e:
            self.log(f"Error in theme selector: {str(e)}")
            self.log(traceback.format_exc())
            return None

    def save_screenshot(self, theme_name, screenshot_url, headers):
        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')
        theme_folder_path = os.path.join(screenshots_path, theme_name)
        os.makedirs(theme_folder_path, exist_ok=True)
        response = requests.get(screenshot_url, headers=headers).content
        screenshot_path = os.path.join(theme_folder_path, 'screenshot.png')
        with open(screenshot_path, 'wb') as f:
            f.write(response)
        return os.path.join('repo_screenshots', theme_name, 'screenshot.png')

    def fetch_themes(self):
        themes = {}
        screenshots_path = os.path.join(self._pwny_root, 'ui/web/static/repo_screenshots')
        try:
            if os.path.exists(screenshots_path):
                shutil.rmtree(screenshots_path)
            headers = {"Authorization": f"Bearer {self.gittoken}"} if self.gittoken else {}
            response = requests.get(THEMES_REPO, headers=headers)
            response.raise_for_status()
            for item in response.json():
                if item["type"] == "dir":
                    theme_name = item["name"]
                    self.log(f"Fetching theme: {theme_name}")
                    theme_url = item["url"]
                    themes[theme_name] = {"info": None, "screenshot": None}
                    theme_contents = requests.get(theme_url, headers=headers).json()
                    for file in theme_contents:
                        if file["name"] == "info.json":
                            info_url = file["download_url"]
                            info_data = requests.get(info_url, headers=headers).json()
                            themes[theme_name]["info"] = info_data
                        elif file["name"] == "img" and file["type"] == "dir":
                            img_folder_url = file["url"]
                            img_contents = requests.get(img_folder_url, headers=headers).json()
                            if isinstance(img_contents, list):
                                for img_file in img_contents:
                                    if isinstance(img_file, dict) and img_file.get("name") == "screenshot.png":
                                        local_screenshot_path = self.save_screenshot(theme_name, img_file["download_url"], headers)
                                        themes[theme_name]["screenshot"] = local_screenshot_path
            sorted_themes = dict(sorted(themes.items(), key=lambda item: item[0].lower()))
            self.log("Themes fetched successfully:")
            for theme, info in sorted_themes.items():
                version = info["info"].get("version") if info["info"] else "Unknown"
                self.log(f"{theme}: Version {version}, Screenshot: {info['screenshot']}")
            return sorted_themes

        except requests.RequestException as e:
            logging.error(f"Error fetching themes: {e}")
            return {}

    def theme_downloader(self, theme_name):
        try:
            headers = {"Authorization": f"Bearer {self.gittoken}"} if self.gittoken else {}
            theme_contents_url = os.path.join(THEMES_REPO, theme_name)
            response = requests.get(theme_contents_url, headers=headers)
            response.raise_for_status()
            contents = response.json()
            temp_dir = tempfile.mkdtemp()
            temp_theme_path = os.path.join(temp_dir, theme_name)
            final_path = os.path.join(self._plug_root, "themes", theme_name)
            os.makedirs(temp_theme_path, exist_ok=True)
            def download_content(contents, current_path):
                for item in contents:
                    item_path = os.path.join(current_path, item['name'])
                    if item['type'] == 'dir':
                        os.makedirs(item_path, exist_ok=True)
                        dir_response = requests.get(item['url'], headers=headers)
                        dir_response.raise_for_status()
                        download_content(dir_response.json(), item_path)
                    else:
                        file_response = requests.get(item['download_url'], headers=headers)
                        file_response.raise_for_status()
                        with open(item_path, 'wb') as f:
                            f.write(file_response.content)
            download_content(contents, temp_theme_path)
            if os.path.exists(final_path):
                shutil.rmtree(final_path)
            shutil.move(temp_theme_path, final_path)
            shutil.rmtree(temp_dir)
            self.log(f"Theme {theme_name} downloaded successfully to {final_path}")

        except requests.RequestException as e:
            logging.error(f"Error downloading themes: {e}")
            logging.error(traceback.format_exc())
            if 'temp_dir' in locals():
                shutil.rmtree(temp_dir)

    def save_active_config(self, data):
        cfg_path = self.cfg_path
        self.log(f"Saving active config to: {self.cfg_path}")
            
        if os
Download .txt
gitextract_z1nv9f52/

├── .github/
│   ├── FUNDING.yml
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── Fancygotchi.py
├── README.md
├── config.toml
└── fancyshow.py
Download .txt
SYMBOL INDEX (116 symbols across 2 files)

FILE: Fancygotchi.py
  class FancyDisplay (line 2242) | class FancyDisplay:
    method __new__ (line 2245) | def __new__(cls, *args, **kwargs):
    method __init__ (line 2250) | def __init__(self, enabled=False, fps=1, th_path='', mode='screen_save...
    method _start_loop (line 2274) | def _start_loop(self):
    method start (line 2286) | def start(self, res, rot, col):
    method stop (line 2301) | def stop(self):
    method screen_controller (line 2311) | async def screen_controller(self):
    method is_running (line 2317) | def is_running(self):
    method cleanup (line 2323) | def cleanup(self):
    method _calculate_aspect_ratio (line 2336) | def _calculate_aspect_ratio(self, width, height, aspect_ratio):
    method screen (line 2345) | def screen(self):
    method refacer (line 2348) | async def refacer(self):
    method display_hijack (line 2377) | def display_hijack(self):
    method glitch_text_effect (line 2418) | def glitch_text_effect(self, text, glitch_chance=0.2, max_spaces=3):
    method set_mode (line 2431) | def set_mode(self, mode, sub_mode=None, config={}):
    method switch_mode (line 2445) | def switch_mode(self, direction='next'):
    method find_fb_device (line 2466) | def find_fb_device(self):
    method get_fb_size (line 2473) | def get_fb_size(self):
    method read_fb (line 2482) | def read_fb(self, width, height):
    method terminal_mode (line 2486) | def terminal_mode(self):
    method convert_to_rgb (line 2501) | def convert_to_rgb(self, fb_data, width, height):
    method set_screen_saver_mode (line 2515) | def set_screen_saver_mode(self, sub_mode):
    method switch_screen_saver_submode (line 2563) | def switch_screen_saver_submode(self, direction='next'):
    method get_mode_image (line 2583) | def get_mode_image(self):
    method get_screen_saver_image (line 2595) | def get_screen_saver_image(self):
    method auxiliary_image (line 2612) | def auxiliary_image(self):
    method show_logo (line 2627) | def show_logo(self):
    method moving_shapes_screen_saver (line 2654) | def moving_shapes_screen_saver(self):
    method random_colors_screen_saver (line 2699) | def random_colors_screen_saver(self):
    method hyperdrive_screen_saver (line 2707) | def hyperdrive_screen_saver(self):
    method show_animation_screen_saver (line 2754) | def show_animation_screen_saver(self):
  class FancyMenu (line 2823) | class FancyMenu:
    method __init__ (line 2824) | def __init__(self, fancygotchi, menu_theme, custom_menus={}):
    method reset_menus (line 2843) | def reset_menus(self, custom_menus={}):
    method load_menu_config (line 2851) | def load_menu_config(self, config):
    method populate_plugins_menu (line 2878) | def populate_plugins_menu(self,plugin_names):
    method populate_themes_menu (line 2894) | def populate_themes_menu(self):
    method toggle (line 2911) | def toggle(self):
    method navigate (line 2916) | def navigate(self, direction):
    method select (line 2931) | def select(self):
    method check_timeout (line 2935) | def check_timeout(self):
    method render (line 2947) | def render(self):
    method scroll_text (line 3197) | def scroll_text(self, draw, menu_item_key, color, scrolltext, scrollfo...
  class Menu (line 3225) | class Menu:
    method __init__ (line 3226) | def __init__(self, name, items, back_reference="Main menu"):
    method navigate (line 3244) | def navigate(self, direction):
    method add_button (line 3248) | def add_button(self, title, action):
  function menu_contains_button (line 3251) | def menu_contains_button(menu, button_name):
  function check_internet_and_repo (line 3287) | def check_internet_and_repo():
  function get_all_plugin_names (line 3309) | def get_all_plugin_names(fancygotchi):
  function is_int (line 3316) | def is_int(s):
  function box_to_xywh (line 3323) | def box_to_xywh(position):
  function adjust_image (line 3339) | def adjust_image(image_path, zoom, mask=False, refine=150, alpha=False, ...
  function invert_pixels (line 3372) | def invert_pixels(image):
  function alphamask (line 3385) | def alphamask(src_image):
  function masking (line 3398) | def masking(src_image, refine):
  function image_mode (line 3415) | def image_mode(canvas, image, mode):
  function verify_font_info (line 3464) | def verify_font_info(ft):
  function allowed_file (line 3481) | def allowed_file(filename):
  function unzip_file (line 3485) | def unzip_file(zip_file, extract_to):
  function serializer (line 3490) | def serializer(obj):
  function _compile_po_to_mo (line 3495) | def _compile_po_to_mo(po_file_path):
  class Fancygotchi (line 3576) | class Fancygotchi(plugins.Plugin):
    method __init__ (line 3583) | def __init__(self):
    method adjust_code (line 3880) | def adjust_code(self, file_path, changes):
    method check_and_fix_fb (line 3911) | def check_and_fix_fb(self):
    method zram_check (line 3968) | def zram_check(self):
    method fps_check (line 3984) | def fps_check(self):
    method log (line 3994) | def log(self, msg):
    method on_ready (line 4014) | def on_ready(self, agent):
    method on_loaded (line 4018) | def on_loaded(self):
    method on_unload (line 4022) | def on_unload(self, ui):
    method on_ui_setup (line 4099) | def on_ui_setup(self, ui):
    method cleanup_display (line 4123) | def cleanup_display(self):
    method _share_state (line 4131) | def _share_state(self, ui):
    method button_controller (line 4142) | def button_controller(self, cmd=None, screen=1):
    method navigate_fancymenu (line 4180) | def navigate_fancymenu(self, cmd=None):
    method on_ui_update (line 4207) | def on_ui_update(self, ui):
    method generate_default_config (line 4407) | def generate_default_config(self, config_path, actual_state):
    method refresh_plugins (line 4439) | def refresh_plugins(self):
    method load_and_run_module (line 4453) | def load_and_run_module(self, module_path):
    method process_actions (line 4476) | def process_actions(self, command):
    method theme_creator (line 4576) | def theme_creator(self, theme_name, state, oriented=False, resolution=...
    method theme_selector (line 4642) | def theme_selector(self, config, boot=False):
    method save_screenshot (line 4748) | def save_screenshot(self, theme_name, screenshot_url, headers):
    method fetch_themes (line 4758) | def fetch_themes(self):
    method theme_downloader (line 4798) | def theme_downloader(self, theme_name):
    method save_active_config (line 4835) | def save_active_config(self, data):
    method theme_save_config (line 4845) | def theme_save_config(self, theme, rotation):
    method reload_voice (line 4859) | def reload_voice(self, ui, lang=None):
    method setup_menu (line 4912) | def setup_menu(self, th_menu):
    method theme_update (line 4944) | def theme_update(self, ui, boot=False):
    method theme_list (line 5174) | def theme_list(self):
    method change_font (line 5202) | def change_font(self, old_font, new_font=None, size_offset=None):
    method theme_export (line 5209) | def theme_export(self, theme_name):
    method get_font_path (line 5232) | def get_font_path(self, font_name):
    method setup_font (line 5238) | def setup_font(self, bold, bold_small, medium, huge, bold_big, small):
    method rgba_text (line 5246) | def rgba_text(self, text, tfont, color='black', width=None, height=None):
    method add_widget (line 5288) | def add_widget(self, ui, key, widget_type, th_widget):
    method get_face_path (line 5472) | def get_face_path(self, img_path, face, image_type):
    method configure_widget (line 5493) | def configure_widget(self, ui, key, widget_type):
    method remove_widgets (line 5600) | def remove_widgets(self, ui):
    method pwncanvas_creation (line 5614) | def pwncanvas_creation(self, res):
    method pos_convert (line 5650) | def pos_convert(self, x, y, w, h, r=None, r0=None, r1=None):
    method paste_image (line 5769) | def paste_image(self, img, x, y):
    method paste_value (line 5779) | def paste_value(self, value, pos, text_font, color, wrap=None):
    method drawer (line 5793) | def drawer(self):
    method ui2 (line 6029) | def ui2(self):
    method on_webhook (line 6046) | def on_webhook(self, path, request):

FILE: fancyshow.py
  class Fancyshow (line 9) | class Fancyshow(plugins.Plugin):
    method __init__ (line 15) | def __init__(self):
    method on_loaded (line 35) | def on_loaded(self):
    method on_unload (line 41) | def on_unload(self, ui):
    method on_ui_setup (line 85) | def on_ui_setup(self, ui):
    method on_ui_update (line 89) | def on_ui_update(self, ui):
    method _get_initial_options (line 155) | def _get_initial_options(self, ui):
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (323K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 945,
    "preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 1063,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG]\"\nlabels: ''\nassignees: ''\n\n---\n\n**Pwnagotch"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": "Fancygotchi.py",
    "chars": 288836,
    "preview": "# adding api or ui attribute to know the actual theme config and or fancy state\n\nimport argparse\nimport asyncio\nimport c"
  },
  {
    "path": "README.md",
    "chars": 2029,
    "preview": "# <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/78d"
  },
  {
    "path": "config.toml",
    "chars": 1075,
    "preview": "main.plugins.Fancygotchi.enabled = true #<-- Fancygotchi will generate a default config \nmain.plugins.Fancygotchi.rotati"
  },
  {
    "path": "fancyshow.py",
    "chars": 10677,
    "preview": "import logging\nimport time\nimport pwnagotchi.plugins as plugins\n\n# This plugin is designed to demonstrate how to use the"
  }
]

About this extraction

This page contains the full source code of the V0r-T3x/Fancygotchi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (298.1 KB), approximately 67.8k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!