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 %}
{% endblock %}
{% block styles %}
{{ super() }}
{% endblock %}
{% block content %}
Second hardware display
Pwnagotchi hardware display
Next second screen mode
Previous second screen mode
Next screen saver
Previous screen saver
L2
R2
L1
↑
X
R1
←
→
Y
A
↓
Select
Start
B
Theme Description
Select a theme:
Select Theme
Theme Description
No configuration for the default theme
Save Configuration
Configuration editor
Config Path
CSS editor
CSS Path
Info editor
Info Path
Reset Pwnagotchi core CSS
Theme editor
Coming soon !
If you like the project feel free to contribute !
{% for line in logo.splitlines() %}{{ line }}
{% endfor %}
Confirm Deletion
Are you sure you want to delete the selected theme?
Cancel
Delete
▲
{% 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, ">");
}
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 = '' + escapeHtml(data.css) + '
';
document.querySelector("#config h4:nth-of-type(3)").innerText = data.info_path;
var infoContent = document.getElementById("Info");
infoContent.innerHTML = '' + escapeHtml(data.info) + '
';
}
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 = $('').val('Default').text('Default');
selectElement.append(defaultOption);
themes.forEach(function(theme) {
var 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('' + theme.toUpperCase() + ' ');
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 = '' + value + ' ';
$themeDescriptionContent.append($('').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 = $('').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('' + theme.toUpperCase() + ' ');
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 = '' + value + ' ';
var listItem = $('').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(' 1:
lines = text.split('\n')
max_char = 0
tot_char = 0
for line in lines:
tot_char = tot_char + len(line)
char_line = len(line)
if char_line > max_char: max_char = char_line
w = int(w / (tot_char / max_char))
imgtext = Image.new('1', (w,h), 0xff)
dt = ImageDraw.Draw(imgtext)
dt.text((0,0), text, font=tfont, fill=0x00)
if color == 0: color = 'black'
imgtext = ImageOps.colorize(imgtext.convert('L'), black = color, white = 'white')
imgtext = imgtext.convert("RGBA")
data = imgtext.getdata()
newData = []
for item in data:
if item[0] in range(250, 256) and item[1] in range(250, 256) and item[2] in range(250, 256):
newData.append((255, 255, 255, 0))
else:
newData.append(item)
imgtext.putdata(newData)
imgtext = imgtext.convert('RGBA')
return imgtext
except Exception as e:
self.log(f"Error while rgba_text ({text}; {tfont}; {color}; {width}; {height}): {str(e)}")
self.log(traceback.format_exc())
return None
def add_widget(self, ui, key, widget_type, th_widget):
conf = 0
th_opt = self._theme['theme']['options']
if key in self._state:
if key in th_widget:
if self.orientation == 'vertical' and th_widget[key].get('position-v'):
if self._state[key]['position'] != tuple(th_widget[key]['position-v']):
self._state[key]['position'] = tuple(th_widget[key]['position-v'])
elif th_widget[key].get('position'):
if self._state[key]['position'] != tuple(th_widget[key]['position']):
self._state[key]['position'] = tuple(th_widget[key]['position'])
else:
if self._state[key]['position'] != tuple(ui._state.get_attr(key, 'xy')):
position = ui._state.get_attr(key, 'xy')
position = tuple(int(coord) for coord in position)
if len(position) >= 3:
position = box_to_xywh(position)
self._state[key]['position'] = position
if key in th_widget and th_widget[key].get('color'):
if self._state[key]['color'] != th_widget[key]['color']:
self._state[key]['color'] = th_widget[key]['color']
self._state[key]['icolor'] = 0
elif "base_text_color" in th_opt and th_opt['base_text_color']:
if self._state[key]['color'] != th_opt['base_text_color']:
self._state[key]['color'] = th_opt['base_text_color']
self._state[key]['icolor'] = 0
else:
if self._state[key]['color'] != [ui._state.get_attr(key, 'color')]:
self._state[key]['color'] = [ui._state.get_attr(key, 'color')]
self._state[key]['icolor'] = 0
elif key not in self._state:
conf = 1
self._state[key] = {}
self._state_default[key] = {}
self._state_default[key] = copy.deepcopy(self._state[key])
default_values = self.widget_defaults.get(widget_type, {})
self._state[key] = copy.deepcopy(default_values)
self._state[key].update({'widget_type': widget_type})
self._state_default[key].update({'widget_type': widget_type})
position = ui._state.get_attr(key, 'xy')
position = tuple(int(coord) for coord in position)
if len(position) >= 3:
position = box_to_xywh(position)
self._state[key].update({'position': position})
self._state_default[key].update({'position': position})
if key in th_widget:
if self.orientation == 'vertical':
if th_widget[key].get('position-v'):
self._state[key].update({'position': tuple(th_widget[key]['position-v'])})
elif th_widget[key].get('position'):
self._state[key].update({'position': tuple(th_widget[key]['position'])})
elif th_widget[key].get('position'):
self._state[key].update({'position': tuple(th_widget[key]['position'])})
self._state_default[key].update({'color': [ui._state.get_attr(key, 'color')]})
if key in th_widget and th_widget[key].get('color'):
self._state[key].update({'color': th_widget[key]['color']})
self._state[key].update({'icolor': 0})
elif th_opt.get('base_text_color'):
self._state[key].update({'color': th_opt['base_text_color']})
self._state[key].update({'icolor': 0})
else:
self._state[key].update({'color': [ui._state.get_attr(key, 'color')]})
self._state[key].update({'icolor': 0})
if key in th_widget and 'z_axis' in th_widget[key]:
self._state[key].update({'z_axis': th_widget[key]['z_axis']})
if widget_type == 'Text' or widget_type == 'LabeledValue':
if key in th_widget and th_widget[key].get('text_font'):
if th_widget[key].get('text_font') == '' or th_widget[key].get('text_font') == None:
self._state[key].pop('text_font', None)
else:
self._state[key].update({'text_font': th_widget[key]['text_font']})
if widget_type == 'Text':
self._state_default[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'font'))})
if widget_type == 'LabeledValue':
self._state_default[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'text_font'))})
if key in th_widget and th_widget[key].get('text_font_size'):
self._state[key].update({'text_font_size': th_widget[key]['text_font_size']})
else:
if widget_type == 'Text':
self._state[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'font'))})
if widget_type == 'LabeledValue':
self._state[key].update({'text_font_size': verify_font_info(ui._state.get_attr(key, 'text_font'))})
if key in th_widget and th_widget[key].get('size_offset'):
self._state[key].update({'size_offset': th_widget[key]['size_offset']})
if key in th_widget and th_widget[key].get('icon'):
self._state[key].update({'icon': th_widget[key]['icon']})
if key in th_widget and th_widget[key].get('icon_color'):
self._state[key].update({'icon_color': th_widget[key]['icon_color']})
if key in th_widget and th_widget[key].get('invert'):
self._state[key].update({'invert': th_widget[key]['invert']})
if key in th_widget and th_widget[key].get('alpha'):
self._state[key].update({'alpha': th_widget[key]['alpha']})
if key in th_widget and th_widget[key].get('crop'):
self._state[key].update({'crop': th_widget[key]['crop']})
if key in th_widget and th_widget[key].get('mask'):
self._state[key].update({'mask': th_widget[key]['mask']})
if key in th_widget and th_widget[key].get('refine'):
self._state[key].update({'refine': th_widget[key]['refine']})
if key in th_widget and th_widget[key].get('zoom'):
self._state[key].update({'zoom': th_widget[key]['zoom']})
if key in th_widget and th_widget[key].get('image_type'):
self._state[key].update({'image_type': th_widget[key]['image_type']})
if widget_type == 'Text':
self._state_default[key].update({'wrap': ui._state.get_attr(key, 'wrap')})
if key in th_widget and th_widget[key].get('wrap'):
self._state[key].update({'wrap': th_widget[key]['wrap']})
else:
self._state[key].update({'wrap': ui._state.get_attr(key, 'wrap')})
self._state[key].update({'max_length': ui._state.get_attr(key, 'max_length')})
if key in th_widget and th_widget[key].get('max_length'):
self._state[key].update({'max_length': th_widget[key]['max_length']})
else:
self._state[key].update({'max_length': ui._state.get_attr(key, 'max_length')})
if key in th_widget and th_widget[key].get('face'):
self._state[key].update({'max_length': th_widget[key]['max_length']})
if widget_type == 'LabeledValue':
self._state_default[key].update({'label': ui._state.get_attr(key, 'label')})
if key in th_widget and 'label' in th_widget[key]:
self._state[key].update({'label': th_widget[key]['label']})
else:
self._state[key].update({'label': ui._state.get_attr(key, 'label')})
if key in th_widget and th_widget[key].get('label_font'):
self._state[key].update({'label_font': th_widget[key]['label_font']})
if key in th_widget and th_widget[key].get('label_font_size'):
self._state[key].update({'label_font_size': th_widget[key]['label_font_size']})
else:
self._state[key].update({'label_font_size': verify_font_info(ui._state.get_attr(key, 'label_font'))})
self._state_default[key].update({'label_spacing': ui._state.get_attr(key, 'label_spacing')})
if key in th_widget and th_widget[key].get('label_spacing'):
self._state[key].update({'label_spacing': th_widget[key]['label_spacing']})
elif 'label_spacing' in th_opt and th_opt['label_spacing']:
self._state[key].update({'label_spacing': th_opt['label_spacing']})
else:
self._state[key].update({'label_spacing': ui._state.get_attr(key, 'label_spacing')})
if key in th_widget and th_widget[key].get('label_line_spacing'):
self._state[key].update({'label_line_spacing': th_widget[key]['label_line_spacing']})
elif 'label_line_spacing' in th_opt and th_opt['label_line_spacing']:
self._state[key].update({'label_line_spacing': th_opt['label_line_spacing']})
else:
self._state[key].update({'label_line_spacing': 0})
if key in th_widget and th_widget[key].get('f_awesome'):
self._state[key].update({'f_awesome': th_widget[key]['f_awesome']})
if key in th_widget and th_widget[key].get('f_awesome_size'):
self._state[key].update({'f_awesome_size': th_widget[key]['f_awesome_size']})
if widget_type == 'Line':
self._state_default[key].update({'width': ui._state.get_attr(key, 'width')})
if key in th_widget and th_widget[key].get('width'):
self._state[key].update({'width': th_widget[key]['width']})
else:
self._state[key].update({'width': ui._state.get_attr(key, 'width')})
if widget_type == 'Bitmap':
self._state[key].update({'f_awesome': False})
if key in th_widget and th_widget[key].get('icon'):
self._state[key].update({'icon': th_widget[key]['icon']})
if key in th_widget and th_widget[key].get('invert'):
self._state[key].update({'invert': th_widget[key]['invert']})
if key in th_widget and th_widget[key].get('alpha'):
self._state[key].update({'alpha': th_widget[key]['alpha']})
if key in th_widget and th_widget[key].get('crop'):
self._state[key].update({'crop': th_widget[key]['crop']})
if key in th_widget and th_widget[key].get('mask'):
self._state[key].update({'mask': th_widget[key]['mask']})
if key in th_widget and th_widget[key].get('refine'):
self._state[key].update({'refine': th_widget[key]['refine']})
if key in th_widget and th_widget[key].get('zoom'):
self._state[key].update({'zoom': th_widget[key]['zoom']})
if key in th_widget and th_widget[key].get('icon_color'):
self._state[key].update({'icon_color': th_widget[key]['icon_color']})
if conf:
self.configure_widget(ui, key, widget_type)
def get_face_path(self, img_path, face, image_type):
variations = [
face,
face.upper(),
face.lower(),
face.capitalize(),
face.replace('-', '_'),
face.replace('_', '-'),
face.replace('-', '_').upper(),
face.replace('_', '-').upper(),
face.replace('-', '_').lower(),
face.replace('_', '-').lower()
]
for variation in variations:
face_path = os.path.join(img_path, f'{variation}.{image_type}')
if os.path.exists(face_path):
return face_path
return None
def configure_widget(self, ui, key, widget_type):
try:
if key == 'face':
self._state[key].update({'face': True})
self._state[key].update({'f_awesome': False})
if self._state[key]['icon']:
face_dict = self._config['ui']['faces']
img_path = os.path.join(self._th_path, 'img', 'face')
for face in face_dict:
image_type = self._state[key].get('image_type', 'png')
if isinstance(face_dict[face], str):
face_path = self.get_face_path(img_path, face, image_type)
if face_path:
if 'face_map' not in self._state[key]:
self._state[key].update({'face_map': {}})
self._state[key]['face_map'].update({
face: [
face_dict[face],
adjust_image(face_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])
]
})
else:
logging.warning(f"[Fancygotchi] No valid face path found for '{face}'")
if key == 'friend_face':
self._state[key].update({'friend_face': True})
face_dict = self._config['ui']['faces']
self._state[key].update({'f_awesome': False})
if self._state[key]['icon']:
face_dict = self._config['ui']['faces']
img_path = os.path.join(self._th_path, 'img', 'friend_face')
for face in face_dict:
image_type = self._state[key].get('image_type', 'png')
if isinstance(face_dict[face], str):
face_path = self.get_face_path(img_path, face, image_type)
if face_path:
if 'friend_face_map' not in self._state[key]:
self._state[key].update({'friend_face_map': {}})
self._state[key]['friend_face_map'].update({
face: [
face_dict[face],
adjust_image(face_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])
]
})
else:
print(f"Warning: No valid face path found for '{face}'")
if self._state[key].get('icon'):
if self._state[key]['icon'] == True:
source = 'label'
if key not in ['face', 'friend_face']:
if widget_type == 'LabeledValue' and not self._state[key]['f_awesome']:
icon_path = os.path.join(self._th_path, 'img', 'widgets', key, self._state[key][source])
self._state[key].update({'icon_image': adjust_image(Image.open(icon_path), self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])})
if not self._state[key]['f_awesome']:
if widget_type == 'Bitmap':
img_path = os.path.join(self._th_path, 'img', 'widgets', key)
files = [f for f in os.listdir(img_path)]
file_count = len(files)
if file_count == 1:
image_path = os.path.join(img_path, files[0])
self._state[key].update({'image': adjust_image(image_path, self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])})
elif file_count > 3 and file_count % 2 == 0:
image_dict = {}
file_names = [os.path.splitext(f)[0] for f in files]
for file in file_names:
if file.endswith('A'):
id_number = file[:-1]
corresponding_b = id_number + 'B'
if corresponding_b in file_names:
original_a = [f for f in files if os.path.splitext(f)[0] == file][0]
original_b = [f for f in files if os.path.splitext(f)[0] == corresponding_b][0]
img_a = Image.open(os.path.join(img_path, original_a))
img_b = adjust_image(Image.open(os.path.join(img_path, original_b)), self._state[key]['zoom'], self._state[key]['mask'], self._state[key]['refine'], self._state[key]['alpha'], self._state[key]['invert'], self._state[key]['crop'])
image_dict[int(id_number)] = [img_a, img_b]
self._state[key].update({'image_dict': image_dict})
img_o, img_c = image_dict[2]
self._state[key].update({'image': img_c})
else:
self.log(f"Error: There are {file_count} images.")
icon_img = Image.new('1', (10, 10), 0x00)
self._state[key].update({'image': icon_img})
else:
fa_path = os.path.join(self._th_path, 'fonts', self.f_awesome_name)
fa = ImageFont.truetype(fa_path, self._state[key]['f_awesome_size'])
try:
code_point = int(self._state[key][source], 16)
except:
self.log("wrong font awesome icon code point: %s" % self._state[key][source])
code_point = int("f00d", 16)
icon = chr(code_point)
w,h = fa.getsize(icon)
icon_img = Image.new('1', (int(w), int(h)), 0xff)
dt = ImageDraw.Draw(icon_img)
dt.text((0,0), icon, font=fa, fill=0x00)
icon_img = icon_img.convert('RGBA')
self._state[key].update({'icon_image': icon_img})
except Exception as e:
self.log("non fatal error while configuring Fancygotchi widget: %s" % e)
self.log(traceback.format_exc())
def remove_widgets(self, ui):
if self._state:
keys_to_delete = []
for key, state in self._state.items():
tag = 0
for k, s in ui._state.items():
if key == k:
tag = 1
if tag == 0:
keys_to_delete.append(key)
for key in keys_to_delete:
self.log(f'remove widget: {key}')
del self._state[key]
def pwncanvas_creation(self, res):
try:
th_opt = self._theme['theme']['options']
rot = self.options['rotation']
if rot == 0 or rot == 180:
self.orientation = 'horizontal'
x, y = res
l = x
w = y
elif rot == 90 or rot == 270:
self.orientation = 'vertical'
x, y = res
l = y
w = x
bg_color = th_opt['bg_color']
if isinstance(bg_color, list):
bg_color = tuple(bg_color)
if not bg_color or bg_color == '':
bg_color = (0,0,0,0)
self._pwncanvas = Image.new('RGBA', (l, w), bg_color)
self._pwndata = Image.new('RGBA', (l, w), (0,0,0,0))
if not self._frames == []:
iframe = self._frames[self._i]
self._pwncanvas.paste(iframe, (0,0), iframe)
if isinstance(self._bg, Image.Image) and self._bg is not None:
self._pwncanvas.paste(self._bg, (0,0), self._bg.convert('RGBA'))
except Exception as e:
logging.error(f"Error in pwncanvas_creation: {e}")
raise
def pos_convert(self, x, y, w, h, r=None, r0=None, r1=None):
rot = self._config.get('main',{}).get('plugins',{}).get('Fancygotchi',{}).get('rotation', 0)
if r is not None:
rot = r
if rot == 0 or rot == 180:
if r0 is not None: width = r0
else: width = self._res[0]
if r1 is not None: height = r1
else: height = self._res[1]
if rot == 90 or rot == 270:
width = self._res[1]
height = self._res[0]
if r1 is not None: width = r1
else: width = self._res[0]
if r0 is not None: height = r0
else: height = self._res[1]
if isinstance(w, str) and '%' in w:
try:
percent_value = float(w.replace('%', ''))
w = (percent_value / 100) * width
except ValueError:
self.log(f"Invalid percentage value for width: {w}")
w = 0
else:
w = int(w)
if isinstance(h, str) and '%' in h:
try:
percent_value = float(h.replace('%', ''))
h = (percent_value / 100) * height
except ValueError:
self.log(f"Invalid percentage value for height: {h}")
h = 0
else:
h = int(h)
top = 0
bottom = height - h
right = width - w
left = width
center_x = (width / 2) - (w / 2)
center_y = (height / 2) - (h / 2)
def replace_keywords(formula, axis):
keyword_mapping = {
"center_x": center_x,
"center_y": center_y,
"left": left,
"right": right,
"top": top,
"bottom": bottom,
"width": width,
"height": height,
"w": w,
"h": h,
}
if axis == 'x':
keyword_mapping["center"] = center_x
keyword_mapping.pop('center_y', None)
keyword_mapping.pop('top', None)
keyword_mapping.pop('bottom', None)
keyword_mapping.pop('height', None)
elif axis == 'y':
keyword_mapping["center"] = center_y
keyword_mapping.pop('center_x', None)
keyword_mapping.pop('left', None)
keyword_mapping.pop('right', None)
keyword_mapping.pop('width', None)
else:
raise ValueError("Invalid axis. Choose 'x' or 'y'.")
for keyword, value in keyword_mapping.items():
if keyword in formula:
formula = formula.replace(keyword, str(value))
return formula
def safe_eval(expr):
try:
if re.search(r'[^0-9\+\-\*/\(\)\. ]', expr):
raise ValueError(f"Invalid expression: {expr}")
result = eval(expr)
return result
except Exception as e:
self.log(f"Error evaluating expression: {expr}. Exception: {e}")
return 0
axis = 'x'
if not is_int(x):
try:
x = replace_keywords(x, axis)
x = safe_eval(x)
except ValueError as e:
self.log(f"Error processing x: {e}")
x = 0
else:
x = int(x)
if x < 0:
x = width + x
axis = 'y'
if not is_int(y):
try:
y = replace_keywords(y, axis)
y = safe_eval(y)
except ValueError as e:
self.log(f"Error processing y: {e}")
y = 0
else:
y = int(y)
if y < 0:
y = height + y
x2 = int(x + w)
y2 = int(y + h)
return int(x), int(y), int(x2), int(y2)
def paste_image(self, img, x, y):
if isinstance(img, Image.Image):
w, h = img.size
x, y, x2, y2 = self.pos_convert(x,y,w,h)
img = img.convert('RGBA')
self._pwndata.paste(img, (x, y, x2, y2), img)
x, y ,x2 ,y2 = self.pos_convert(x, y, w, h)
def paste_value(self, value, pos, text_font, color, wrap=None):
x, y = pos
if wrap and hasattr(self, 'wrapper') and self.wrapper is not None:
try:
text = '\n'.join(self.wrapper.wrap(value))
except AttributeError:
text = value
else:
text = value
imgtext = self.rgba_text(text, text_font, color, self._res[0], self._res[1])
self.paste_image(imgtext, x, y)
def drawer(self):
try:
th_opt = copy.deepcopy(self._default['theme']['options'])
th_opt.update( self._theme['theme']['options'])
draw = ImageDraw.Draw(self._pwndata)
draw_state = dict(sorted(self._state.items(), key=lambda item: item[1].get('z_axis', 0)))
keys_to_remove = {key: value for key, value in draw_state.items() if value.get('z_axis', 0) < 0}
for key in keys_to_remove:
del draw_state[key]
if self.stealth_mode:
keys_to_remove = {key for key, value in draw_state.items() if value.get('z_axis', 0) < 100}
for key in keys_to_remove:
del draw_state[key]
for widget, state in draw_state.items():
if 'wrap' in state:
wrap = state['wrap']
else:
wrap = False
if 'main_text_color' in th_opt and th_opt['main_text_color'] in ([], ""):
if 'color' in state:
color = state['color'][state['icolor']]
else:
color = ['black']
elif 'main_text_color' in th_opt and th_opt['main_text_color'] != []:
color = th_opt['main_text_color'][self._icolor]
if len(state['position']) >= 3:
x, y, w, h = state['position']
x, y, x2, y2 = self.pos_convert(x,y,w,h)
if len(state['position']) == 2:
x, y = state['position']
self.wrapper = TextWrapper(width=state['max_length'], replace_whitespace=False) if wrap else None
if state['widget_type'] == 'Text' or state['widget_type'] == 'LabeledValue':
if state['widget_type'] == 'LabeledValue':
try:
label_font = getattr(self, state["label_font_size"])
label = state['label']
except Exception as e:
label_font = getattr(self, 'Medium')
try:
text_font = getattr(self, state["text_font_size"])
except Exception as e:
text_font = getattr(self, 'Medium')
if 'text_font' in state or 'size_offset' in state:
if 'text_font' in state and state['text_font']:
font = state['text_font']
else:
font = th_opt['status_font']
if 'size_offset' in state:
size_offset = state['size_offset']
else:
size_offset = th_opt['size_offset']
if text_font is not None:
text_font = self.change_font(text_font, font, size_offset)
if state['widget_type'] == 'LabeledValue':
if label_font is not None and state['label'] is not None:
try:
lw, lh = label_font.getsize(label)
except:
_, _, lw, lh = label_font.getbbox(state['label'])
else:
lw, lh = 0, 0
if text_font is not None and state['value'] is not None:
try:
vw, vh = text_font.getsize(state['value'])
except:
_, _, vw, vh = text_font.getbbox(state['value'])
else:
vw, vh = 0, 0
if state['widget_type'] == 'LabeledValue':
total_height = max(lh,vh)+max(0,state['label_line_spacing'])
total_width = lw + state['label_spacing'] + 5 * len(state['label'])
x, y, l_w, l_h = self.pos_convert(x, y, total_width, total_height)
v_y = y + state['label_line_spacing']
v_x = x + state['label_spacing'] + 5 * len(state['label'])
elif state['widget_type'] == 'Text':
v_x, v_y, v_w, v_h = self.pos_convert(x, y, vw, vh)
if state['value'] is not None:
if not state['icon']:
self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)
if state['widget_type'] == 'LabeledValue':
l_text = state['label']
if state['widget_type'] == 'LabeledValue':
imgtext = self.rgba_text(l_text, label_font, color, self._res[0], self._res[1])
self.paste_image(imgtext, x, y)
else:
if 'f_awesome' in state and state['f_awesome'] == False:
if widget not in ['face', 'friend_face']:
self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)
icon_image = state['icon_image']
else:
if widget == 'face':
x = v_x
y = v_y
for face in state['face_map'].items():
face_name, face_map = face
if face_map[0] == state['value']:
icon_image = face_map[1]
elif widget == 'friend_face':
x = v_x
y = v_y
for face in state['friend_face_map'].items():
friend_face_name, friend_face_map = face
if friend_face_map[0] == state['value']:
icon_image = friend_face_map[1]
if 'icon_color' in state and state['icon_color']:
alpha = 0
wht = (255, 255, 255, 255)
if color == 'white': color = (249,249,249,256)
if icon_image.mode in ('RGBA', 'LA') or (icon_image.mode == 'P' and 'transparency' in icon_image.info):
alpha = 1
white_image = Image.new('RGB', icon_image.size, wht)
white_image.paste(icon_image, mask=icon_image.split()[3])
icon_image = white_image
L_image = icon_image.convert('L')
icon_image = ImageOps.colorize(L_image, black = color, white = wht)
if alpha:
icon_image = icon_image.convert('RGBA')
data = icon_image.getdata()
newData = []
for item in data:
if item[0] in range(250, 256) and item[1] in range(250, 256) and item[2] in range(250, 256):
newData.append((255, 255, 255, 0))
else:
newData.append(item)
icon_image.putdata(newData)
icon_image = icon_image.convert('RGBA')
self.paste_image(icon_image, x, y)
else:
if color == 'white': color = (249,249,249,255)
img = state['icon_image'].convert('L')
icon_image = ImageOps.colorize(img, black = color, white = 'white')
icon_image = icon_image.convert('RGBA')
data = icon_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)
icon_image.putdata(newData)
icon_image = icon_image.convert('RGBA')
self.paste_value(state['value'], (v_x,v_y), text_font, color, wrap)
self.paste_image(icon_image, x, y)
elif state['widget_type'] == 'Bitmap':
icon_bmp = state['image']
alpha = 0
original = icon_bmp
iw, ih = icon_bmp.size
v_x, v_y, v_x2, v_y2 = self.pos_convert(state['position'][0], state['position'][1], iw, ih)
if icon_bmp is not None:
if icon_bmp.mode in ('RGBA', 'LA') or (icon_bmp.mode == 'P' and 'transparency' in icon_bmp.info):
alpha = 1
if 'mask' in state and state['mask']:
refine = state['refine']
image = icon_bmp.convert('RGBA')
width, height = image.size
pixels = image.getdata()
new_pixels = []
for pixel in pixels:
r, g, b, a = pixel
if r > 255 - refine and g > 255 - refine and b > 255 - refine:
new_pixel = (255, 255, 255, 0)
else:
new_pixel = (r, g, b, a)
new_pixels.append(new_pixel)
refined_image = Image.new("RGBA", image.size)
refined_image.putdata(new_pixels)
white_image = Image.new('RGB', refined_image.size, (255, 255, 255))
white_image.paste(icon_bmp, mask=refined_image.split()[3])
icon_bmp = white_image
if 'icon_color' in state and state['icon_color']:
L_image = icon_bmp.convert('L')
icon_bmp = ImageOps.colorize(L_image, black = color, white = (255, 255, 255))
if 'alpha' in state and state['alpha']:
if alpha:
icon_bmp = alphamask(icon_bmp)
self.paste_image(icon_bmp, v_x, v_y)
elif state['widget_type'] == 'Line':
draw.line([x,y,x2,y2], fill=color, width=state['width'])
elif state['widget_type'] == 'Rect':
draw.rectangle([x,y,x2,y2], fill=color)
elif state['widget_type'] == 'FilledRect':
draw.rectangle([x,y,x2,y2], fill=color)
if state['icolor'] + 1 >= len(state['color']):
state['icolor'] = 0
else:
state['icolor'] += 1
if self._icolor + 1 >= len(th_opt['main_text_color']):
self._icolor = 0
else:
self._icolor += 1
if hasattr(self, 'fancy_menu'):
if getattr(self.fancy_menu, 'active', False):
menu_img = self.fancy_menu.render()
self.fancy_menu_img = menu_img
if self.fancy_menu_img is not None:
self._pwndata.paste(self.fancy_menu_img, (0, 0, self._pwndata.size[0], self._pwndata.size[1]), self.fancy_menu_img.split()[3])
if self._fg != '':
target_width = self._pwndata.size[0]
target_height = self._pwndata.size[1]
self._fg = self._fg.resize((target_width, target_height), Image.Resampling.LANCZOS)
self._pwndata.paste(self._fg, (0, 0), self._fg)
self._pwncanvas = self._pwncanvas.convert("RGB")
self._pwncanvas.paste(self._pwndata, (0, 0, self._pwndata.size[0], self._pwndata.size[1]),self._pwndata)
self._pwncanvas = self._pwncanvas.convert("RGBA")
except Exception as e:
logging.error(f"Error in Fancygotchi drawer: {e}")
logging.error(traceback.format_exc())
raise
def ui2(self):
try:
image = self.second_screen
if hasattr(self, 'display_controller') and self.display_config['second_screen_webui'] and self.dispHijack:
image = self.display_controller.screen()
img_io = BytesIO()
image.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png'), 200
except Exception as ex:
image = self.second_screen
img_io = BytesIO()
image.save(img_io, 'PNG')
img_io.seek(0)
return send_file(img_io, mimetype='image/png'), 200
def on_webhook(self, path, request):
try:
if not self.ready:
return "Plugin not ready"
if request.method == "GET":
if path == "/" or not path:
themes = sorted(self.theme_list(), key=lambda x: x.lower())
if self._theme_name != 'Default':
css_path = os.path.join(self._th_path, 'style.css')
info_path = os.path.join(self._th_path, 'info.json')
rot = self._config['main']['plugins']['Fancygotchi']['rotation']
if self.cfg_path is not None:
cfg_path = self.cfg_path
else:
cfg_path = 'No custom configuration'
name = self._theme_name
if os.path.exists(css_path):
css = open(css_path, 'r').read()
else:
css = ''
css_path = 'No custom css'
if os.path.exists(info_path):
info = open(info_path, 'r').read()
else:
info = ''
info_path = 'No custom info'
else:
css_path = 'default has no custom css'
info_path = 'default has no custom info'
cfg_path = 'default has no custom configuration'
css = ''
info = ''
name = 'Default'
files = {'CSS': [css_path, css], 'Info': [info_path, info]}
return render_template_string(
INDEX, themes=themes,
default_theme=name,
rotation=self._config['main']['plugins']['Fancygotchi']['rotation'],
author=Fancygotchi.__author__,
version=Fancygotchi.__version__,
files=files,
cfg_path=cfg_path,
name=name,
logo=LOGO,
webui_fps=self.webui_fps,
fancy_repo=FANCY_REPO,
)
elif path == "key":
# curl -X GET "http://changeme:changeme@localhost:8080/plugins/Fancygotchi/key"
return render_template_string("{{ csrf_token() }}"), 200
elif path == "ui2":
return self.ui2()
elif path == "active_theme":
return json.dumps({"theme": self._theme_name})
elif path == "theme_list":
themes = self.theme_list()
return json.dumps(themes)
elif path == "theme_download_list":
self.log("Theme download list fetching started...")
try:
isInternet, msg = check_internet_and_repo()
self.log(f"isInternet: {isInternet}, msg: {msg}")
if isInternet:
themes_dict = self.fetch_themes()
return json.dumps({"status": 200, "data": themes_dict}), 200
else:
return json.dumps({"error": msg}), 500
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return json.dumps({"error": "Theme download list error"}), 500
elif str(path).split("/")[0] == "theme_export":
try:
theme_name = path.split("/")[-1]
return self.theme_export(theme_name)
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "theme selection error", 500
elif path == "load_config":
try:
if self.cfg_path is not None:
cfg_path = self.cfg_path
with open(cfg_path, 'r') as f:
config = toml.load(f)
else:
config = {}
cfg_path = ""
if self._theme_name != 'Default':
css_path = os.path.join(self._th_path, 'style.css')
info_path = os.path.join(self._th_path, 'info.json')
if os.path.exists(css_path):
with open(css_path, 'r') as f:
css_content = f.read()
else:
css_content = 'No custom CSS'
if os.path.exists(info_path):
with open(info_path, 'r') as f:
info_content = f.read()
else:
info_content = 'No custom Info'
else:
css_content = 'No custom CSS'
info_content = 'No custom Info'
css_path = 'No custom CSS path'
info_path = 'No custom Info path'
return json.dumps({
"config": config,
"css": css_content,
"info": info_content,
"name": self._theme_name,
"cfg_path": cfg_path,
"css_path": css_path,
"info_path": info_path,
})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Error loading configuration", 500
elif path == "display_hijack":
try:
self.dispHijack = True
return json.dumps({"message": "Hijack display successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Display hijacking error", 500
elif path == "display_pwny":
try:
self.dispHijack = False
return json.dumps({"message": "Pwny change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Display Pwny error", 500
elif path == "second_screen":
logging.warning("second_screen")
try:
self.dispHijack = not self.dispHijack
return json.dumps({"message": "Second screen change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Display Pwny error", 500
elif path == "display_next":
try:
self.process_actions({"action": "switch_screen_mode"})
return json.dumps({"message": "Display change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Display next error", 500
elif path == "display_previous":
try:
self.process_actions({"action": "switch_screen_mode_reverse"})
return json.dumps({"message": "Display change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Display previous error", 500
elif path == "screen_saver_next":
try:
self.process_actions({"action": "next_screen_saver"})
return json.dumps({"message": "Screen saver change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Next screen saver error", 500
elif path == "screen_saver_previous":
try:
self.process_actions({"action": "previous_screen_saver"})
return json.dumps({"message": "Screen saver change successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "previous screen saver error", 500
elif path == "stealth":
try:
self.process_actions({"action": "stealth_mode"})
return json.dumps({"message": "Stealth mode successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Stealth mode error", 500
elif path == "theme_refresh":
try:
self.process_actions({"action": "theme_refresh"})
return json.dumps({"message": "Theme refresh successful!", "status": 200}), 200
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Theme refresh error", 500
elif path == "theme_select":
try:
theme = request.args.get('theme')
rotation = request.args.get('rotation')
rot = int(rotation)
self.theme_save_config(theme, rot)
self.refresh = True
return json.dumps({"message": "theme selection successful!", "status": 200})
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "theme selection error", 500
elif path == "plugin":
try:
# Retrieve 'name' and 'enable' parameters from the query string
name = request.args.get('name')
enable = request.args.get('enable', 'false').lower() == 'true'
if not name:
return json.dumps({"message": "Plugin name is missing.", "status": 400}), 400
# Process the toggle action
self.process_actions({"action": "plugin", "name": name, "enable": enable})
# Respond with success message
return json.dumps({"message": f"Plugin '{name}' toggled {'enabled' if enable else 'disabled'} successfully!", "status": 200})
except Exception as ex:
logging.error(f"Error toggling plugin: {ex}")
logging.error(traceback.format_exc())
return json.dumps({"message": "Plugin toggle error", "status": 500}), 500
elif path == "btn_cmd":
try:
screen = 1
action = request.args.get('action')
hardware = request.args.get('hardware')
scr = request.args.get('screen')
self.log(f"screen: {screen}")
if scr is None:
if self.dispHijack:
screen = 2
else:
screen = scr
self.log(f"btn_cmd: {action}")
self.log(f"hardware: {hardware}")
action_mapping = {
'up': 'btn_up',
'down': 'btn_down',
'left': 'btn_left',
'right': 'btn_right',
'select': 'btn_select',
'start': 'btn_start',
'a': 'btn_a',
'b': 'btn_b',
'x': 'btn_x',
'y': 'btn_y',
'l1': 'btn_l1',
'l2': 'btn_l2',
'r1': 'btn_l1',
'r2': 'btn_l2',
}
btn_action = action_mapping.get(action)
self.log(f"btn_cmd: {btn_action}")
if btn_action:
self.button_controller({"action": btn_action}, screen=screen)
#self.navigate_fancymenu({"action": btn_action})
return json.dumps({"message": f"{btn_action} successful!", "status": 200})
else:
return "Invalid navigation action", 400
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Navigation error", 500
elif path == "reset_css":
try:
original_css_backup = os.path.join(self._pwny_root, 'ui/web/static/css/style.css.backup')
with open(original_css_backup, 'w+') as f:
f.write(CSS)
return json.dumps({"message": "CSS reset successful!", "status": 200})
except Exception as e:
self.log(f"Error: {e}")
return "Error resetting CSS", 500
elif request.method == "POST":
if path == "version_compare":
is_newer = None
local_version = None
try:
jreq = request.get_json()
response = json.loads(json.dumps(jreq))
theme = response['theme']
version = response['version']
self.log(f'Download selection: theme {theme} version {version}')
info_path = os.path.join(self._plug_root, "themes", theme, "info.json")
if not os.path.exists(info_path):
logging.error(f"Theme {theme} not found locally.")
else:
with open(info_path, 'r') as f:
local_info = json.load(f)
local_version = local_info.get('version')
if local_version is None:
self.log(f"Local version not found for theme {theme}.")
local_version = 'Unknown'
self.log(f"Local theme version: {local_version}")
is_newer = version > local_version if local_version != 'Unknown' else False
self.log(f'Is the online theme newer: {is_newer}')
return json.dumps({
'is_newer': is_newer,
'local_version': local_version
}), 200
except Exception as ex:
logging.error(f"Error handling theme version: {ex}")
logging.error(traceback.format_exc())
return json.dumps({'error': 'Theme version error'}), 500
if path == "theme_download_select":
try:
jreq = request.get_json()
response = json.loads(json.dumps(jreq))
theme = response['theme']
self.log(f'Download selection: theme {theme}')
self.theme_downloader(theme)
return "success", 200
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return json.dumps({'error': f"theme download error: {ex}"}) , 500
elif path == "save_config":
try:
data = request.get_json()
config = data.get('config')
css = data.get('css')
info = data.get('info')
theme_cfg = {}
theme_cfg['theme'] = config['theme']
self.save_active_config(theme_cfg)
if self._th_path:
css_src = os.path.join(self._th_path, 'style.css')
info_src = os.path.join(self._th_path, 'info.json')
if info != "No custom Info":
if os.path.exists(info_src):
os.remove(info_src)
with open(info_src, 'w') as info_file:
info_file.write(info)
self.log(f"Updated Info files at {self._th_path}")
if css != "No custom CSS":
if os.path.exists(css_src):
os.remove(css_src)
with open(css_src, 'w') as css_file:
css_file.write(css)
self.log(f"Updated CSS files at {self._th_path}")
self.log(f"Updated CSS and Info files at {self._th_path}")
self.refresh = True
return "Configuration saved successfully", 200
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Error saving configuration", 500
elif path == "create_theme":
try:
data = request.get_json()
theme_name = data['theme_name']
use_resolution = data['use_resolution']
use_orientation = data['use_orientation']
is_created = self.theme_creator(theme_name, state=self._state, oriented=use_orientation, resolution=use_resolution)
if is_created:
return "Theme created successfully", 200
else:
return "Theme with same name is existing", 500
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Error creating theme", 500
elif path == "theme_copy":
try:
data = request.get_json()
theme = data['theme']
new_name = data['new_name']
themes_folder = os.path.join(self._plug_root, 'themes')
src_path = os.path.join(themes_folder, theme)
dst_path = os.path.join(themes_folder, new_name)
if os.path.exists(dst_path):
self.log(f"Theme '{theme}' already exists. Skipping creation.")
return "Theme with same name is existing", 500
if os.path.exists(src_path):
copytree(src_path, dst_path)
return "Theme copied successfully", 200
else:
return "Source theme not found", 404
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Error copying theme", 500
elif path == "theme_rename":
try:
data = request.get_json()
theme = data['theme']
new_name = data['new_name']
themes_folder = os.path.join(self._plug_root, 'themes')
src_path = os.path.join(themes_folder, theme)
dst_path = os.path.join(themes_folder, new_name)
if os.path.exists(dst_path):
self.log(f"Theme '{theme}' already exists. Skipping creation.")
return "Theme with same name is existing", 500
if os.path.exists(src_path):
os.rename(src_path, dst_path)
return "Theme renamed successfully", 200
else:
return "Theme not found", 404
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "Error renaming theme", 500
elif path == "theme_upload":
try:
if 'zipFile' in request.files:
file = request.files['zipFile']
if file.filename == '':
return 'No selected file', 400
if file and allowed_file(file.filename):
filename = file.filename
themepath = os.path.join(self._plug_root, 'themes')
filepath = os.path.join(themepath, filename)
file.save(filepath)
with tempfile.TemporaryDirectory() as temp_dir:
unzip_file(filepath, temp_dir)
folders_in_zip = [
name for name in os.listdir(temp_dir)
if os.path.isdir(os.path.join(temp_dir, name))
]
existing_folders = []
for folder in folders_in_zip:
target_folder = os.path.join(themepath, folder)
if os.path.exists(target_folder):
existing_folders.append(folder)
if existing_folders:
return f'{existing_folders} folders were not copied because they already exist.', 400
for folder in folders_in_zip:
source = os.path.join(temp_dir, folder)
target = os.path.join(themepath, folder)
copytree(source, target)
return 'Zip file uploaded and extracted successfully', 200
else:
return 'Invalid file type', 400
else:
return 'No file part in the request', 400
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return 'Theme upload error', 500
elif path == "theme_delete":
try:
jreq = request.get_json()
response = json.loads(json.dumps(jreq))
theme = response['theme']
if theme in ['', self._theme_name]:
self.log('theme can\'t be deleted')
return "theme can't be deleted", 500
else:
themepath = os.path.join(self._plug_root, 'themes')
filepath = os.path.join(themepath, theme)
self.log(f'Delete theme at {filepath}')
os.system(f'rm -r {filepath}')
return "success"
except Exception as ex:
logging.error(ex)
logging.error(traceback.format_exc())
return "theme selection error", 500
elif path == "theme_info":
jreq = request.get_json()
response = json.loads(json.dumps(jreq))
theme = response['theme']
descpath = os.path.join(self._plug_root, 'themes', theme, 'info.json')
info = {
"author": "Unknown",
"version": "Unknown",
"display": "Unknown",
"plugins": "Unknown",
"notes": "Unknown"
}
if theme in ['Default', None]:
descpath = None
info = {
"author": "V0rT3x ",
"version": "1.0.0",
"display": "All",
"plugins": "all",
"notes": "Default theme"
}
try:
if descpath is not None:
if descpath and os.path.exists(descpath):
with open(descpath, 'r') as json_file:
info = json.load(json_file)
theme_info = info
return json.dumps(theme_info, default=serializer), 200
except Exception as ex:
logging.error(f"Error in theme info: {str(ex)}")
logging.error(traceback.format_exc())
return "theme selection error", 500
except Exception as e:
self.log(f"Error in webhook: {str(e)}")
self.log(traceback.format_exc())
return None
================================================
FILE: README.md
================================================
# 🪄FANCYGOTCHI 2.0🖌️
*Fancygotchi is the ultimate theme manager for pwnagotchi*
> "Are you ready to be refaced!?"
> - *Fancygotchi, 2024.*
## Disclaimer
> From **V0r-T3x** (inspired by [**roodriiigooo**](https://github.com/roodriiigooo)): The content here is free for use, but it doesn't mean you can use it however you want. No author or contributor assumes responsibility for the misuse of this device, project, or any component herein. The project and modifications were **developed solely for educational purposes**.
> Any files, plugins or modifications of this project or original project found here should **not be sold**. In the case of use in open projects, videos or any form of dissemination, please remember to give credit to the repository ♥
*I'm in active development, if you want encourage me, become a [Patreon](https://patreon.com/v0rt3x_workshop) or send me a voluntary contribution with [Paypal](https://www.paypal.com/paypalme/v0r73x?country.x=CA&locale.x=en_US)*
# ** The raspberry pi zero W need a fix to run, you need to use [`/pwnagotchi/plugins/__init__.py`](https://github.com/Sniffleupagus/pwnagotchi-snflpgs/blob/snflpgs/pwnagotchi/plugins/__init__.py) **
# ** The rpi4 need some additional steps to access all the features, this will be documented soon **
# :books: The documentation is available in the [FANCYGOTCHI WIKI](https://github.com/V0r-T3x/fancygotchi/wiki)
# :art: The theme v2.0 are available [FANCYGOTCHI 2.0 THEMES](https://github.com/V0r-T3x/Fancygotchi_themes/tree/main/fancygotchi_2.0)
================================================
FILE: config.toml
================================================
main.plugins.Fancygotchi.enabled = true #<-- Fancygotchi will generate a default config
main.plugins.Fancygotchi.rotation = 0 # 0 or 180 is horizontal mode, 90 or 270 is vertival mode
main.plugins.Fancygotchi.theme = "" #<-- if empty the default theme will be loaded
main.plugins.Fancygotchi.fancyserver = false # Fancyserver is used for FancyMenu and for additional control. It is disabled by default.
#ui.fps = 1 #<-- need to have an higher value than 0.0
#ui.display.enabled = true
#ui.display.rotation = 0 #<-- This value need to stay 0
#ui.display.type = "displayhatmini" #<-- select your display
#fs.memory.mounts.data.enabled = false #<-- need to be false to avoid the errno 30 and the tmp file become read only and the pwnagotchi can't save his data
#fs.memory.mounts.data.mount = "/var/tmp/pwnagotchi"
#fs.memory.mounts.data.size = "50M"
#fs.memory.mounts.data.sync = 3600
#fs.memory.mounts.data.zram = false #<-- need to be false to avoid the errno 30 and the tmp file become read only and the pwnagotchi can't save his data
#fs.memory.mounts.data.rsync = true
================================================
FILE: fancyshow.py
================================================
import logging
import time
import pwnagotchi.plugins as plugins
# This plugin is designed to demonstrate how to use the Fancygotchi's partial update feature (ui._update).
# It modifies the 'name' widget's position and color, verifies the changes, and reverts them upon unload.
class Fancyshow(plugins.Plugin):
__author__ = 'V0r-T3x'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'An example plugin to show how to use Fancygotchi\'s ui._update feature.'
def __init__(self):
logging.debug("fancyshow plugin created")
# A dictionary to store options for the plugin (not used in this example but good practice).
self.options = dict()
# This will store the original properties of the widget we are modifying.
self.original_widget_options = {}
# A flag to ensure we only set our desired position once.
self.position_set = False
# Flags to control the plugin's state, especially during unload.
self.unloaded = False
self.reverting = False
# The new widget properties we want to apply.
self.widget_options = {
'position': ["center", "center"],
'color': ["lime", "red"]
}
# A counter to track if the UI state is consistently different from what we expect.
self.discrepancy_counter = 0
# called when the plugin is loaded
def on_loaded(self):
logging.info("fancyshow plugin loaded.")
# Reset the unloaded flag when the plugin is loaded/reloaded.
self.unloaded = False
# called when the plugin is unloaded
def on_unload(self, ui):
logging.info("fancyshow plugin unloading...")
self.reverting = True
# Check if we have stored original options to revert to.
if self.original_widget_options and hasattr(ui, '_update'):
# Use Fancygotchi's partial update mechanism to revert the 'name' widget's properties.
ui._update.update({
'update': True,
'partial': True,
'dict_part': {'widget': {'name': self.original_widget_options}}
})
logging.info(f"fancyshow: attempting to revert 'name' options to {self.original_widget_options}")
# Wait for the UI to confirm the change, with a timeout.
timeout = 10 # seconds
start_time = time.time()
reverted = False
while time.time() - start_time < timeout:
# The `ui.fancy._state` attribute is provided by Fancygotchi and contains the current theme state.
# We check this to verify that our reversion request has been processed.
if hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state and self.original_widget_options:
name_state = ui.fancy._state['name']
all_reverted = all(
# Normalize lists to tuples for comparison, handles nested lists if any.
(tuple(current) if isinstance(current, list) else current) ==
(tuple(expected_val) if isinstance(expected_val, list) else expected_val)
for prop, expected_val in self.original_widget_options.items()
for current in [name_state.get(prop)])
if all_reverted:
logging.info("fancyshow: successfully reverted 'name' position.")
reverted = True
break
time.sleep(0.5) # Check every half-second
if not reverted:
logging.warning(f"fancyshow: could not verify 'name' position was reverted within {timeout} seconds.")
# Final cleanup of state variables for a clean unload.
self.unloaded = True
self.original_widget_options = {}
self.position_set = False
# This is called by Pwnagotchi when the UI is being set up.
# called to set up the ui elements
def on_ui_setup(self, ui):
self._get_initial_options(ui)
# called when the ui is updated
def on_ui_update(self, ui):
if self.unloaded or self.reverting:
# Do nothing if the plugin is in the process of unloading.
return
# Get original position if we haven't already
if not self.original_widget_options:
self._get_initial_options(ui)
# If we still don't have it, we can't do anything yet.
if not self.original_widget_options:
return
# This block runs only once to apply the new widget options.
if not self.position_set and hasattr(ui, '_update') and not ui._update.get('update', False):
# Request a partial theme update to change the 'name' widget.
ui._update.update({
'update': True,
'partial': True,
'dict_part': {'widget': {'name': self.widget_options}}
})
self.position_set = True
logging.info(f"fancyshow: setting 'name' options to {self.widget_options}")
# This block runs on subsequent updates to verify that our changes have been applied and have persisted.
# The UI state can be changed by other plugins or theme updates, so this check is important.
# After setting, we can verify its position on subsequent updates
if self.position_set and hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state:
name_state = ui.fancy._state['name']
for prop, expected_value in self.widget_options.items():
current_value = name_state.get(prop)
# Normalize lists to tuples for comparison
if isinstance(expected_value, list):
expected_value = tuple(expected_value)
if isinstance(current_value, list):
current_value = tuple(current_value)
if current_value != expected_value:
# If the current state doesn't match our expected state, increment a counter.
# This might happen temporarily if another plugin is also updating the UI.
self.discrepancy_counter += 1
logging.warning(f"fancyshow: 'name' property '{prop}' is {current_value}, not {expected_value} as expected. Discrepancy count: {self.discrepancy_counter}")
if self.discrepancy_counter >= 15:
logging.warning("fancyshow: Discrepancy threshold reached. Investigating cause...")
# Check if a pending partial update is the cause of the discrepancy
if hasattr(ui, '_update') and ui._update.get('update') and ui._update.get('partial'):
pending_changes = ui._update.get('dict_part', {}).get('widget', {}).get('name', {})
if pending_changes:
# Compare pending changes to our saved original options
if any(pending_changes.get(p) != self.original_widget_options.get(p) for p in pending_changes):
logging.info("fancyshow: Detected a pending theme change. Updating baseline and re-applying options.")
# The pending update is the new baseline
self.original_widget_options.update(pending_changes)
else:
# No pending update, so re-check the current state from the UI
logging.info("fancyshow: No pending update detected. Re-evaluating original state from UI.")
self._get_initial_options(ui)
# Reset the flag to force re-application of widget_options
self.position_set = False
# Reset the counter
self.discrepancy_counter = 0
# Break the loop to allow re-application in the next on_ui_update cycle
break
# A helper function to safely get the initial properties of the 'name' widget.
def _get_initial_options(self, ui):
# `ui.fancy._state` is a special attribute injected by Fancygotchi that holds the current theme's state.
if hasattr(ui, 'fancy') and hasattr(ui.fancy, '_state') and 'name' in ui.fancy._state:
name_state = ui.fancy._state['name']
new_original_options = {}
changed = False
# We only care about the properties we intend to modify.
for prop in self.widget_options.keys():
if prop in name_state:
current_original_value = name_state.get(prop)
new_original_options[prop] = current_original_value
# Check if the baseline has changed since we last checked
if self.original_widget_options and self.original_widget_options.get(prop) != current_original_value:
changed = True
else:
logging.warning(f"fancyshow: could not find original value for property '{prop}'.")
# If this is the first time, store the fetched options.
if not self.original_widget_options:
self.original_widget_options = new_original_options
logging.info(f"fancyshow: Stored initial 'name' options: {self.original_widget_options}")
elif changed:
# Check for false positive: if the "new" origin is what we're trying to set,
# it's likely our own change being read back. Don't update the baseline.
is_false_positive = all(
tuple(new_original_options.get(prop)) == tuple(val) if isinstance(val, list) else new_original_options.get(prop) == val
for prop, val in self.widget_options.items()
)
if is_false_positive:
logging.warning("fancyshow: Detected a potential false positive. The new baseline matches widget_options. Not updating original options.")
else:
logging.warning(f"fancyshow: Original 'name' options changed from {self.original_widget_options} to {new_original_options}. Updating baseline.")
self.original_widget_options = new_original_options
else:
logging.debug("fancyshow: Re-checked original 'name' options. No changes to baseline.")