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">▲</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, "<")
.replace(/>/g, ">");
}
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
gitextract_z1nv9f52/ ├── .github/ │ ├── FUNDING.yml │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── Fancygotchi.py ├── README.md ├── config.toml └── fancyshow.py
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.