Fügt CLI-Toolverwaltung und erweiterte UI-Funktionen hinzu

Ermöglicht die Verwaltung (Erstellen, Auflisten, Ausführen) von Tools über eine Befehlszeilenschnittstelle für Automatisierung und Integration. Die Benutzeroberfläche wurde mit dedizierten Ansichten für Toolerstellung und -bearbeitung, In-App-Lua-Hilfe sowie kontextsensitiven Menüs erweitert. Neue Abhängigkeiten (CLI11, rang) wurden integriert und eine umfassende Testsuite hinzugefügt. Die Lizenzierung wurde auf AGPL-3.0 umgestellt und ein Kontributionsleitfaden bereitgestellt.
This commit is contained in:
2026-07-17 17:22:15 +02:00
parent 074fa73992
commit c611b008af
45 changed files with 5501 additions and 709 deletions

25
.gitignore vendored
View File

@@ -1,8 +1,21 @@
/.idea/
/cmake-build-debug/
/cmake-build-debug-visual-studio/
/cmake-build-release-visual-studio/
/cmake-build-minsizerel-visual-studio/
/installer/.idea/
# Build output
/cmake-build-*/
/out_embed/
/embedded/
# IDE / Editor
/.idea/
/installer/.idea/
/.vs/
/.vscode/
*.user
# AI assistants
/.claude/
/.junie/
# Cache
/.cache/
# Windows pseudo-files
/NUL

6
.gitmodules vendored
View File

@@ -19,3 +19,9 @@
[submodule "lib/saucer4lua"]
path = lib/saucer4lua
url = https://git.schmidti.digital/Yadciel/saucer4lua.git
[submodule "lib/CLI11"]
path = lib/CLI11
url = https://github.com/CLIUtils/CLI11.git
[submodule "lib/rang"]
path = lib/rang
url = https://github.com/agauniyal/rang.git

View File

@@ -21,6 +21,9 @@ add_subdirectory(lib/saucer)
add_subdirectory(lib/lua)
add_subdirectory(lib/sol2)
add_subdirectory(lib/toolbox_win)
add_subdirectory(lib/CLI11)
add_library(rang INTERFACE)
target_include_directories(rang INTERFACE lib/rang/include)
# add_subdirectory(lib/saucer4lua)
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS
@@ -38,6 +41,7 @@ endif ()
add_executable(Toolbox
main.cpp
cli/main.cpp
${APP_SOURCES}
${APP_HEADERS}
)
@@ -55,6 +59,10 @@ target_include_directories(Toolbox
lib/c_tray
)
if (MSVC)
target_compile_options(Toolbox PRIVATE /utf-8)
endif ()
if (WIN32)
target_compile_definitions(Toolbox PRIVATE NOMINMAX UNICODE _UNICODE)
@@ -100,14 +108,78 @@ target_link_libraries(Toolbox
tray
nlohmann_json
cppcodec
CLI11::CLI11
rang
toolbox_win
saucer::embedded
)
if (WIN32)
target_link_libraries(Toolbox PRIVATE shell32 user32 gdi32)
target_link_libraries(Toolbox PRIVATE shell32 user32 gdi32 shlwapi)
endif ()
if (WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
set_target_properties(Toolbox PROPERTIES WIN32_EXECUTABLE TRUE)
include(CTest)
if (BUILD_TESTING)
add_executable(toolbox-template-tests
tests/toolTemplatesTests.cpp
src/toolTemplates.cpp
)
target_include_directories(toolbox-template-tests PRIVATE inc)
target_link_libraries(toolbox-template-tests PRIVATE sol2::sol2 lua::lib)
if (MSVC)
target_compile_options(toolbox-template-tests PRIVATE /utf-8)
endif ()
add_test(NAME toolbox-template-contract COMMAND toolbox-template-tests)
add_executable(toolbox-settings-tests
tests/toolRegistrySettingsTests.cpp
src/toolRegistry.cpp
src/toolTemplates.cpp
)
target_include_directories(toolbox-settings-tests PRIVATE inc)
target_link_libraries(toolbox-settings-tests PRIVATE nlohmann_json cppcodec)
if (MSVC)
target_compile_options(toolbox-settings-tests PRIVATE /utf-8)
endif ()
add_test(NAME toolbox-settings-contract COMMAND toolbox-settings-tests)
add_executable(toolbox-lua-runner-tests
tests/luaRunnerTests.cpp
src/luaRunner.cpp
src/toolLaunchBridge.cpp
)
target_include_directories(toolbox-lua-runner-tests PRIVATE inc)
target_link_libraries(toolbox-lua-runner-tests PRIVATE sol2::sol2 lua::lib)
if (WIN32)
target_link_libraries(toolbox-lua-runner-tests PRIVATE shell32)
endif ()
if (MSVC)
target_compile_options(toolbox-lua-runner-tests PRIVATE /utf-8)
endif ()
add_test(NAME toolbox-lua-runner-contract COMMAND toolbox-lua-runner-tests)
find_package(Python3 COMPONENTS Interpreter QUIET)
if (Python3_Interpreter_FOUND)
add_test(
NAME toolbox-cli-contract
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/toolbox_cli_test.py $<TARGET_FILE:Toolbox>
)
endif ()
find_program(NODE_EXECUTABLE node)
if (NODE_EXECUTABLE)
add_test(
NAME toolbox-runtime-ui-contract
COMMAND ${NODE_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/toolRuntime.test.js
)
endif ()
endif ()
if (WIN32)
# Toolbox is a console-subsystem binary so -h/list/create/run write to the
# inherited stdout/stderr. In GUI mode the console window is hidden at runtime.
set_target_properties(Toolbox PROPERTIES
WIN32_EXECUTABLE FALSE
LINK_FLAGS "/ENTRY:\"wmainCRTStartup\""
)
endif ()

59
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,59 @@
# Toolbox Contributing
Vielen Dank, dass du zu Toolbox beitragen möchtest! Dieses Dokument
beschreibt die Bedingungen, unter denen Beiträge angenommen werden.
## Contributor License Agreement (CLA)
Damit wir Beiträge in das Projekt integrieren können, wird jeder
Contributor eineGenehmigung erteilt, dass der Code abgetreten wird
wenn die dazu entwickeln.
Indem du einen Pull Request, Patch oder Commit einreichst, stimmst du
folgendem zu:
1. **Beitrag unter eigenen Bedingungen.** Du bestätigst, dass der
eingereichte Code dein eigenes Werk ist oder du die uneingeschränkte
Berechtigung hast, ihn unter der GNU AGPL-3.0-or-later weiterzugeben.
2. **Übertragbare Lizenz (CLA).** Du räumst dem Projektinhaber
(Yadciel / schmidti.digital) eine dauerhafte, weltweite,
gebührenfreie, nicht-exklusive, übertragbare und unterlizenzierbare
Lizenz ein, deinen Beitrag in jeglicher Form zu nutzen, zu
modifizieren und weiterzuverteilen unabhängig von der
AGPL-Lizenz des Gesamtprojekts.
3. **Keine Verletzung von Rechten Dritter.** Du garantiertest, dass dein
Beitrag keine Patente, Marken, Urheberrechte oder andere Rechte
Dritter verletzt.
4. **Keine Vertraulichkeit.** Dein Beitrag ist kein vertrauliches
Material deines Arbeitgebers oder eines Dritten. Falls ein Arbeitgeber
Rechte an deinem Beitrag geltend macht, haftest du selbst dafür, dies
vor der Einreichung rechtlich zu klären.
## WoContributor License Agreement (CLA)
Für alle akzeptierten Beiträge gilt: Du erteilst mit der Einreichung
die oben genannte Genehmigung. Die Einreichung erfolgt elektronisch über
den Pull-Request-Mechanismus des Repository-Hosters (z. B. Git schmidti.digital
oder GitHub). Ein zusätzlicher physischer oder digitaler Vertrag ist nicht
erforderlich die Einreichung selbst gilt als CLA-Zustimmung.
## Entwicklung
```bash
# Debug-Build
cmake -S . -B cmake-build-debug && cmake --build cmake-build-debug --config Debug
# Tests
ctest --test-dir cmake-build-debug --output-on-failure -C Debug
# Release-Build
cmake -S . -B cmake-build-release && cmake --build cmake-build-release --config Release
```
## Lizenz
Dieses Projekt steht unter der GNU Affero General Public License v3.0
oder später (AGPL-3.0-or-later). Siehe [LICENSE](LICENSE).

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
For the full license text, see https://www.gnu.org/licenses/agpl-3.0.txt
or the file COPYING.AGPL3 which should accompany this distribution.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@@ -1,5 +1,49 @@
# Toolbox
Eine modulare Werkzeug-Sammlung für Windows mit Lua-Scripting,
WebView2-basiertem GUI und CLI.
## Lizenz
Dieses Projekt steht unter der **GNU Affero General Public License v3.0
oder später** (AGPL-3.0-or-later).
- [LICENSE](LICENSE) AGPL-3.0 Lizenztext
- [CONTRIBUTING.md](CONTRIBUTING.md) Contributor License Agreement (CLA)
Contributor erteilen mit der Einreichung von Code eine Genehmigung, dass
ihr Beitrag vom Projektinhaber unter beliebigen Lizenzen weiterverwendet
werden darf (CLA, siehe CONTRIBUTING.md).
## Abhängigkeiten und Lizenzen
| Bibliothek | Lizenz | Quelle |
|---|---|---|
| saucer | MIT | https://github.com/saucer/saucer |
| sol2 | MIT | https://github.com/ThePhD/sol2 |
| lua-cmake | MIT | https://github.com/lubgr/lua-cmake |
| c_tray | MIT | https://github.com/binRick/c_tray |
| nlohmann/json | MIT | https://github.com/nlohmann/json |
| cppcodec | MIT | https://github.com/tplgy/cppcodec |
| CLI11 | BSD-3-Clause | https://github.com/CLIUtils/CLI11 |
| rang | Unlicense | https://github.com/agauniyal/rang |
| Prism.js | MIT | https://prismjs.com (Lea Verou) |
| coco | MIT | https://github.com/Curve/coco |
| saucer-fill | MIT | (CPM, saucer-Abhängigkeit) |
| saucer-embed | MIT | (CPM, saucer-Abhängigkeit) |
| glaze | MIT | (CPM, saucer-Abhängigkeit) |
| lockpp | MIT | (CPM, saucer-Abhängigkeit) |
| packageproject | MIT | (CPM, saucer-Abhängigkeit) |
| ereignis | MIT | (CPM, saucer-Abhängigkeit) |
| flagpp | MIT | (CPM, saucer-Abhängigkeit) |
| polo | MIT | (CPM, saucer-Abhängigkeit) |
| rebind | MIT | (CPM, saucer-Abhängigkeit) |
| saucer4lua | proprietär/inhouse | https://git.schmidti.digital/Yadciel/saucer4lua |
| toolbox_win | proprietär/inhouse | (projekt-intern) |
Alle MIT-, BSD-3- und Unlicense-Bibliotheken sind mit der AGPL-3.0
kompatibel.
## install
first install node.js
@@ -16,4 +60,17 @@ like
saucer embed D:/Repos/Toolbox/res/html D:/Repos/Toolbox/out_embed
## Agent CLI
`Toolbox.exe` provides a machine-readable JSON interface without starting the GUI:
```text
Toolbox.exe list
Toolbox.exe create --name "My Tool" --description "Example" --script-file start.lua --timeout 30
Toolbox.exe run "My Tool"
Toolbox.exe -h
```
Use `--home <path>` before the command to target a different user directory. `-h` and `--help` show the human-readable, colored help. Colors are selected automatically and can be overridden with `--color always` or `--color never`. Operational commands continue to write exactly one JSON object to stdout and use a non-zero exit code on failure.
Tools are trusted local Lua programs. `run` exposes the same `os`, `io`, and `package` libraries as the Toolbox GUI, so agents should only execute reviewed tools.

View File

@@ -79,3 +79,21 @@ cmd /c "call ""C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxili
- Tests fuer Tool-Scanning ohne echte User-Home-Abhaengigkeit.
- Debug/Release-Smoke-Build lokal und spaeter in CI.
- Optional HTML-Lint oder DOM-Test fuer Renderfunktionen.
### 8. Saucer-Fenster abstrahieren
- Gemeinsamen Factory-/Lifecycle-Helper fuer `saucer::window` und `saucer::smartview` einfuehren.
- Pro Fenster einen expliziten WebView2-Storage-Pfad vergeben, damit weitere Fenster nicht in der Environment-Erzeugung haengen.
- Embedded Resources, Navigation-Policy, Icon, Hintergrund, DevTools und Close-/Hide-Verhalten zentral konfigurieren.
- `show()`, `focus()`, Positionierung und Wiederverwendung als einheitlichen Fenster-Workflow kapseln.
- Einen kleinen nativen Smoke-Harness fuer das Erzeugen und Anzeigen eines zweiten Fensters bereitstellen.
### 9. Agentenschnittstelle
Status: erste CLI-Schnittstelle umgesetzt.
- Eigenes `toolbox-cli`-Target, damit Agenten nicht vom GUI-/Saucer-Lifecycle abhaengen.
- Maschinenlesbare JSON-Ausgabe und belastbare Exit-Codes fuer `list`, `create` und `run`.
- `--home` erlaubt isolierte Agentenlaeufe und Tests ohne Zugriff auf das echte Benutzerverzeichnis.
- Als naechsten Schritt gemeinsame synchrone Lua-Ausfuehrungslogik fuer GUI und CLI extrahieren, damit Timeout-, Library- und Fehler-Policy nicht auseinanderlaufen.
- Optional spaeter MCP als duenner Adapter ueber dieselben Core-Operationen anbieten.

42
THIRD_PARTY_LICENSES.md Normal file
View File

@@ -0,0 +1,42 @@
# Third-Party Licenses
Dieses Verzeichnis enthält die Lizenztexte der eingebetteten
Drittanbieter-Bibliotheken, die nicht als Git-Submodule verwaltet werden.
## Prism.js (MIT)
Prism.js by Lea Verou MIT License
Siehe: res/vendor/prism/LICENSE.txt
## Eingebettete CPM-Pakete (alle MIT)
Folgende Pakete werden zur Build-Zeit über CPM heruntergeladen
und statisch gelinkt. Alle stehen unter der MIT-Lizenz:
- coco (https://github.com/Curve/coco)
- saucer-fill (saucer-Abhängigkeit)
- saucer-embed (saucer-Abhängigkeit)
- glaze (saucer-Abhängigkeit)
- lockpp (saucer-Abhängigkeit)
- packageproject (saucer-Abhängigkeit)
- ereignis (saucer-Abhängigkeit)
- flagpp (saucer-Abhängigkeit)
- polo (saucer-Abhängigkeit)
- rebind (saucer-Abhängigkeit)
Alle MIT-lizenzierten Bibliotheken sind mit der AGPL-3.0 kompatibel.
## Submodule-Lizenzen
Die Git-Submodule bringen ihre eigenen LICENSE-Dateien mit:
| Submodule | Lizenz | Lizenzdatei |
|---|---|---|
| lib/saucer | MIT | lib/saucer/LICENSE |
| lib/sol2 | MIT | lib/sol2/LICENSE.txt |
| lib/lua (lua-cmake) | MIT | lib/lua/LICENSE |
| lib/c_tray | MIT | lib/c_tray/LICENSE |
| lib/json (nlohmann/json) | MIT | lib/json/LICENSE.MIT |
| lib/cppcodec | MIT | lib/cppcodec/LICENSE |
| lib/CLI11 | BSD-3-Clause | lib/CLI11/LICENSE |
| lib/rang | Unlicense | lib/rang/LICENSE |

400
cli/main.cpp Normal file
View File

@@ -0,0 +1,400 @@
#include "toolboxCli.h"
#include "toolConfig.h"
#include "toolLaunchBridge.h"
#include "toolRegistry.h"
#include <CLI/CLI.hpp>
#include <nlohmann/json.hpp>
#include <rang.hpp>
#include <sol/sol.hpp>
extern "C" {
#include <lauxlib.h>
#include <lua.h>
}
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <optional>
#include <ranges>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace {
using json = nlohmann::json;
struct LuaHookContext {
std::chrono::steady_clock::time_point deadline;
};
void timeoutHook(lua_State* state, lua_Debug*) {
const auto* context = *static_cast<LuaHookContext**>(lua_getextraspace(state));
if (context && context->deadline != (std::chrono::steady_clock::time_point::max)() &&
std::chrono::steady_clock::now() > context->deadline) {
luaL_error(state, "Lua script timed out");
}
}
std::filesystem::path defaultHome() {
#ifdef _WIN32
for (const wchar_t* variable : {L"USERPROFILE", L"HOME"}) {
const DWORD required = GetEnvironmentVariableW(variable, nullptr, 0);
if (required <= 1) {
continue;
}
std::wstring value(required, L'\0');
const DWORD written = GetEnvironmentVariableW(variable, value.data(), required);
if (written > 0 && written < required) {
value.resize(written);
return std::filesystem::path(value);
}
}
#else
if (const char* profile = std::getenv("USERPROFILE")) {
return profile;
}
if (const char* home = std::getenv("HOME")) {
return home;
}
#endif
return std::filesystem::current_path();
}
#ifdef _WIN32
std::string wideToUtf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (length <= 0) {
throw std::runtime_error("Command line contains invalid Unicode.");
}
std::string result(static_cast<std::size_t>(length), '\0');
if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), result.data(), length, nullptr, nullptr) <= 0) {
throw std::runtime_error("Command line contains invalid Unicode.");
}
return result;
}
#endif
void printJson(const json& value) {
std::cout << value.dump() << '\n';
}
int fail(std::string message, int code = 2) {
printJson({{"ok", false}, {"error", std::move(message)}});
return code;
}
std::optional<std::string> optionValue(const std::vector<std::string>& args, std::string_view option) {
const auto it = std::ranges::find(args, option);
if (it == args.end() || std::next(it) == args.end()) {
return std::nullopt;
}
return *std::next(it);
}
std::optional<int> parseInt(const std::optional<std::string>& value) {
if (!value) {
return std::nullopt;
}
try {
std::size_t consumed = 0;
const int result = std::stoi(*value, &consumed);
if (consumed != value->size()) {
return std::nullopt;
}
return result;
} catch (...) {
return std::nullopt;
}
}
std::optional<std::string> readTextFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
return std::nullopt;
}
std::ostringstream content;
content << input.rdbuf();
return content.str();
}
const ToolboxItem* findTool(const std::vector<ToolboxItem>& tools, const std::string& name) {
const auto it = std::ranges::find(tools, name, &ToolboxItem::name);
return it == tools.end() ? nullptr : &*it;
}
int listTools(const std::filesystem::path& home) {
json tools = json::array();
for (const auto& tool : ToolRegistry::loadTools(home)) {
tools.push_back({{"name", tool.name}, {"description", tool.description}});
}
printJson({{"ok", true}, {"tools", std::move(tools)}});
return 0;
}
int createTool(const std::filesystem::path& home, const std::vector<std::string>& args) {
const auto name = optionValue(args, "--name");
if (!name) {
return fail("create requires --name <name>");
}
ToolDraft draft;
draft.name = *name;
draft.description = optionValue(args, "--description").value_or("");
draft.luaScript = optionValue(args, "--script").value_or("");
if (const auto scriptFile = optionValue(args, "--script-file")) {
const auto script = readTextFile(std::filesystem::u8path(*scriptFile));
if (!script) {
return fail("Script file could not be read: " + *scriptFile);
}
draft.luaScript = *script;
}
if (const auto timeoutValue = optionValue(args, "--timeout")) {
const auto timeout = parseInt(timeoutValue);
if (!timeout) {
return fail("--timeout must be an integer");
}
draft.timeoutSeconds = *timeout;
}
const auto result = ToolRegistry::createTool(home, draft);
if (!result.success) {
return fail(result.message);
}
printJson({{"ok", true}, {"message", result.message}, {"tool", draft.name}});
return 0;
}
json executeLua(const std::filesystem::path& scriptPath, int timeoutSeconds) {
sol::state lua;
lua.open_libraries(sol::lib::base,
sol::lib::os,
sol::lib::string,
sol::lib::coroutine,
sol::lib::package,
sol::lib::table,
sol::lib::math,
sol::lib::utf8,
sol::lib::io);
ToolLaunchBridge::registerFunctions(lua);
std::string output;
sol::function tostring = lua["tostring"];
lua.set_function("print", [&output, tostring](sol::variadic_args values) mutable {
bool first = true;
for (const auto value : values) {
if (!first) {
output.push_back('\t');
}
first = false;
const sol::protected_function_result converted = tostring(value);
if (converted.valid()) {
output += converted.get<std::string>();
}
}
output.push_back('\n');
});
LuaHookContext context{
timeoutSeconds <= 0
? (std::chrono::steady_clock::time_point::max)()
: std::chrono::steady_clock::now() + std::chrono::seconds(timeoutSeconds),
};
lua_State* state = lua.lua_state();
*static_cast<LuaHookContext**>(lua_getextraspace(state)) = &context;
lua_sethook(state, timeoutHook, LUA_MASKCOUNT, 10000);
const auto script = readTextFile(scriptPath);
if (!script) {
return {{"ok", false}, {"error", "Lua script could not be read."}};
}
sol::protected_function_result result = lua.safe_script(*script, sol::script_pass_on_error);
if (!result.valid()) {
const sol::error error = result;
return {{"ok", false}, {"error", error.what()}};
}
return {{"ok", true}, {"output", std::move(output)}};
}
int runTool(const std::filesystem::path& home, const std::vector<std::string>& args) {
if (args.empty()) {
return fail("run requires a tool name");
}
const auto tools = ToolRegistry::loadTools(home);
const ToolboxItem* tool = findTool(tools, args.front());
if (!tool) {
return fail("Tool not found: " + args.front());
}
if (tool->lua_script_path_string.empty()) {
return fail("Tool has no start.lua: " + tool->name);
}
const ToolConfigStore config;
const auto toolPath = std::filesystem::u8path(tool->path);
const int timeout = std::clamp(
config.getInt(toolPath / "config.ini", "lua_timeout_seconds", 30), 0, 86400);
json result = executeLua(std::filesystem::u8path(tool->lua_script_path_string), timeout);
result["tool"] = tool->name;
printJson(result);
return result["ok"].get<bool>() ? 0 : 1;
}
void configureColor(std::string_view mode) {
if (mode == "always") {
rang::setControlMode(rang::control::Force);
rang::setWinTermMode(rang::winTerm::Ansi);
} else if (mode == "never") {
rang::setControlMode(rang::control::Off);
} else {
rang::setControlMode(rang::control::Auto);
rang::setWinTermMode(rang::winTerm::Auto);
}
}
void printHelp() {
std::cout
<< rang::style::bold << rang::fgB::cyan
<< " TOOLBOX CLI" << rang::style::reset << rang::fg::reset << '\n'
<< rang::fg::gray << " Lokale Lua-Werkzeuge verwalten und ausfuehren."
<< rang::fg::reset << "\n\n"
<< rang::style::bold << rang::fgB::yellow << "VERWENDUNG"
<< rang::style::reset << rang::fg::reset << '\n'
<< " toolbox-cli [--home <pfad>] [--color <modus>] <befehl> [optionen]\n\n"
<< rang::style::bold << rang::fgB::yellow << "BEFEHLE"
<< rang::style::reset << rang::fg::reset << '\n'
<< rang::fgB::green << " list" << rang::fg::reset
<< " Alle Werkzeuge als JSON auflisten.\n"
<< rang::fgB::green << " create" << rang::fg::reset
<< " Ein neues Werkzeug erstellen.\n"
<< rang::fgB::green << " run" << rang::fg::reset
<< " Ein Werkzeug ausfuehren.\n\n"
<< rang::style::bold << rang::fgB::yellow << "OPTIONEN"
<< rang::style::reset << rang::fg::reset << '\n'
<< " -h, --help Diese Hilfe anzeigen.\n"
<< " --home <pfad> Alternatives Toolbox-Benutzerverzeichnis.\n"
<< " --color <modus> auto, always oder never (Standard: auto).\n\n"
<< rang::style::bold << rang::fgB::yellow << "BEISPIELE"
<< rang::style::reset << rang::fg::reset << '\n'
<< rang::fg::cyan << " toolbox-cli list\n"
<< " toolbox-cli create --name \"Mein Tool\" --script-file start.lua\n"
<< " toolbox-cli run \"Mein Tool\"" << rang::fg::reset << "\n\n"
<< rang::fg::gray
<< " list, create und run liefern weiterhin genau ein JSON-Objekt."
<< rang::fg::reset << '\n';
}
} // namespace
int runCli(std::vector<std::string> args) {
CLI::App app{"Lokale Lua-Werkzeuge verwalten und ausfuehren.", "toolbox-cli"};
app.set_help_flag("-h,--help", "Diese Hilfe anzeigen.");
app.require_subcommand(1);
app.fallthrough();
std::string homeValue;
std::string colorMode = "auto";
app.add_option("--home", homeValue, "Alternatives Toolbox-Benutzerverzeichnis.");
app.add_option("--color", colorMode, "Farbmodus: auto, always oder never.")
->check(CLI::IsMember({"auto", "always", "never"}));
auto* listCommand = app.add_subcommand("list", "Alle Werkzeuge als JSON auflisten.");
auto* helpCommand = app.add_subcommand("help", "Diese Hilfe anzeigen.");
std::string createName;
std::string createDescription;
std::string createScript;
std::string createScriptFile;
int createTimeout = 0;
auto* createCommand = app.add_subcommand("create", "Ein neues Werkzeug erstellen.");
createCommand->add_option("--name", createName, "Name des Werkzeugs.")->required();
auto* descriptionOption = createCommand->add_option(
"--description", createDescription, "Beschreibung des Werkzeugs.");
auto* scriptOption = createCommand->add_option("--script", createScript, "Lua-Quelltext.");
auto* scriptFileOption = createCommand->add_option(
"--script-file", createScriptFile, "Lua-Datei einlesen.");
auto* timeoutOption = createCommand->add_option(
"--timeout", createTimeout, "Zeitlimit in Sekunden.");
std::string runName;
auto* runCommand = app.add_subcommand("run", "Ein Werkzeug ausfuehren.");
runCommand->add_option("name", runName, "Name des Werkzeugs.")->required();
if (args.empty()) {
configureColor(colorMode);
printHelp();
return 0;
}
if (const auto requestedColor = optionValue(args, "--color")) {
configureColor(*requestedColor);
}
std::ranges::reverse(args);
try {
app.parse(args);
} catch (const CLI::CallForHelp&) {
printHelp();
return 0;
} catch (const CLI::ParseError& error) {
return fail(error.what());
}
configureColor(colorMode);
const std::filesystem::path home = homeValue.empty()
? defaultHome()
: std::filesystem::u8path(homeValue);
if (*helpCommand) {
printHelp();
return 0;
}
if (*listCommand) {
return listTools(home);
}
if (*createCommand) {
std::vector<std::string> createArgs{"--name", createName};
if (descriptionOption->count() > 0) {
createArgs.insert(createArgs.end(), {"--description", createDescription});
}
if (scriptOption->count() > 0) {
createArgs.insert(createArgs.end(), {"--script", createScript});
}
if (scriptFileOption->count() > 0) {
createArgs.insert(createArgs.end(), {"--script-file", createScriptFile});
}
if (timeoutOption->count() > 0) {
createArgs.insert(createArgs.end(), {"--timeout", std::to_string(createTimeout)});
}
return createTool(home, createArgs);
}
if (*runCommand) {
return runTool(home, {runName});
}
return fail("No command selected.");
}
// runCli is called from main.cpp when CLI arguments are detected.
// No entry point here.

29
inc/luaHelpWindow.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include "modules/hwnd_module.h"
#include <saucer/app.hpp>
#include <saucer/smartview.hpp>
#include <memory>
class LuaHelpWindow {
public:
explicit LuaHelpWindow(std::shared_ptr<saucer::application> app);
void initialize();
void show();
private:
std::shared_ptr<saucer::application> m_app;
saucer::smartview m_webview;
std::unique_ptr<hwnd_module> m_hwndModule;
HWND m_hwnd{};
void configureWindow();
void loadResources();
void exposeToJs();
void injectDocumentationToolbar();
void showCheatsheet();
void showOfficialDocumentation();
};

View File

@@ -5,23 +5,54 @@
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
class LuaRunner {
public:
using CompletionCallback = std::function<void()>;
using CompletionCallback = std::function<void(bool)>;
void run(int id, const std::filesystem::path& scriptPath, int timeoutSeconds, CompletionCallback onComplete);
void cancel(int id);
struct RunResult {
bool started = false;
bool singleInstance = false;
};
struct TaskStatus {
bool singleInstance = false;
bool running = false;
};
RunResult run(const std::string& taskId, const std::filesystem::path& scriptPath,
int timeoutSeconds, CompletionCallback onComplete);
TaskStatus status(const std::string& taskId, const std::filesystem::path& scriptPath) const;
void cancel(const std::string& taskId) const;
void shutdown() const;
~LuaRunner();
private:
struct LuaTask {
std::shared_ptr<std::atomic_bool> cancel;
std::shared_ptr<std::atomic_bool> executionFinished;
std::shared_ptr<std::atomic_bool> callbackFinished;
bool singleInstance = false;
std::jthread worker;
LuaTask(std::shared_ptr<std::atomic_bool> cancelFlag,
std::shared_ptr<std::atomic_bool> executionFinishedFlag,
std::shared_ptr<std::atomic_bool> callbackFinishedFlag,
bool isSingleInstance)
: cancel(std::move(cancelFlag)),
executionFinished(std::move(executionFinishedFlag)),
callbackFinished(std::move(callbackFinishedFlag)),
singleInstance(isSingleInstance) {}
};
struct State {
std::mutex tasksMutex;
std::unordered_map<int, LuaTask> tasks;
std::unordered_map<std::string, std::vector<LuaTask>> tasks;
};
std::shared_ptr<State> m_state = std::make_shared<State>();

View File

@@ -13,15 +13,16 @@ class optionWindow {
public:
// Konstruktor erhält das Application-Objekt als Parameter
optionWindow(std::shared_ptr<saucer::application>& app,
std::function<int()> getLuaTimeoutSeconds,
std::function<void(int)> setLuaTimeoutSeconds,
std::function<std::string()> getActiveToolName,
std::function<std::string()> getToolConfigIni,
std::function<void(const std::string &)> setToolConfigIni,
std::function<std::string()> getToolDetails,
std::function<std::string(const std::string &)> saveToolDetails,
std::function<std::string()> openToolFolder,
std::function<std::string(const std::string &)> createTool,
std::function<void()> openLuaHelp,
std::function<void()> openIndex);
// Methode zum Anzeigen des Fensters
void show();
void showCreateTool();
// Für die Fensterinitialisierung
void initialize();
@@ -33,11 +34,11 @@ private:
saucer::smartview m_webview;
std::unique_ptr<hwnd_module> m_hwnd_module;
HWND m_hwnd;
std::function<int()> m_getLuaTimeoutSeconds;
std::function<void(int)> m_setLuaTimeoutSeconds;
std::function<std::string()> m_getActiveToolName;
std::function<std::string()> m_getToolConfigIni;
std::function<void(const std::string &)> m_setToolConfigIni;
std::function<std::string()> m_getToolDetails;
std::function<std::string(const std::string &)> m_saveToolDetails;
std::function<std::string()> m_openToolFolder;
std::function<std::string(const std::string &)> m_createTool;
std::function<void()> m_openLuaHelp;
std::function<void()> m_openIndex;
void configureWindow();

11
inc/toolLaunchBridge.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
namespace sol {
class state;
}
namespace ToolLaunchBridge {
void registerFunctions(sol::state& lua);
} // namespace ToolLaunchBridge

View File

@@ -1,6 +1,7 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
@@ -18,11 +19,73 @@ struct ToolboxItem {
std::string license;
};
struct ToolDraft {
std::string name;
std::string description;
std::string luaScript;
std::string iconBase64;
int timeoutSeconds = 30;
std::string templateType = "lua";
std::string templateTarget;
};
struct ToolDetails {
std::filesystem::path path;
std::string name;
std::string description;
std::string luaScript;
std::string iconBase64;
int timeoutSeconds = 30;
std::string templateType = "lua";
std::string templateTarget;
};
struct ToolDetailsResult {
bool success = false;
std::string message;
ToolDetails details;
};
struct ToolUpdateDraft {
std::string name;
std::string description;
std::string luaScript;
std::optional<std::string> iconBase64;
int timeoutSeconds = 30;
std::string templateType = "lua";
std::string templateTarget;
};
struct ToolUpdateResult {
bool success = false;
std::string message;
std::filesystem::path toolPath;
};
struct ToolCreationResult {
bool success = false;
std::string message;
};
struct ToolDeletionResult {
bool success = false;
std::string message;
};
class ToolRegistry {
public:
static std::filesystem::path toolboxDirectory(const std::filesystem::path& userPath);
static bool ensureToolboxDirectory(const std::filesystem::path& userPath);
static std::vector<ToolboxItem> loadTools(const std::filesystem::path& userPath);
static ToolCreationResult createTool(const std::filesystem::path& userPath,
const ToolDraft& draft);
static ToolDetailsResult getToolDetails(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath);
static ToolUpdateResult updateTool(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath,
const ToolUpdateDraft& draft);
static ToolDeletionResult deleteTool(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath);
private:
static void encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item);

27
inc/toolTemplates.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
struct ToolTemplateDraft {
std::string name;
std::string description;
std::string luaScript;
std::string templateType;
std::string templateTarget;
};
struct ToolTemplateResult {
bool success = false;
std::string message;
ToolTemplateDraft draft;
};
class ToolTemplates {
public:
static std::string defaultLuaScript(std::string_view toolName);
static ToolTemplateResult programLauncher(const std::filesystem::path& executable);
static ToolTemplateResult folderLauncher(const std::filesystem::path& folder);
static ToolTemplateResult websiteLauncher(const std::string& url);
};

4
inc/toolboxCli.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
#include <string>
#include <vector>
int runCli(std::vector<std::string> args);

View File

@@ -9,19 +9,18 @@ class ToolboxJsBridge {
public:
struct Bindings {
std::function<std::string()> getToolboxItems;
std::function<void(int)> runLua;
std::function<void(int)> cancelLua;
std::function<std::string(const std::string&)> runLua;
std::function<void(const std::string&)> cancelLua;
std::function<void()> debugPrint;
std::function<void()> openFileSystem;
std::function<void()> openCommandPrompt;
std::function<void(int)> openSettings;
std::function<void()> openAddTool;
std::function<void(const std::string&)> openSettings;
std::function<std::string(const std::string&)> deleteTool;
std::function<void(int)> setOpacity;
std::function<int()> getOpacity;
std::function<void()> openIndex;
std::function<void()> getNewContent;
std::function<int()> getLuaTimeoutSeconds;
std::function<void(int)> setLuaTimeoutSeconds;
std::function<std::string()> getActiveToolName;
std::function<std::string()> getToolConfigIni;
std::function<void(const std::string&)> setToolConfigIni;
};
static void registerBindings(saucer::smartview& webview, Bindings bindings);

View File

@@ -5,6 +5,7 @@
#include <saucer/webview.hpp>
#include <chrono>
#include "luaRunner.h"
#include "luaHelpWindow.h"
#include "optionWindow.h"
#include "resourceLoader.h"
#include "toolConfig.h"
@@ -18,19 +19,16 @@ public:
explicit ToolboxWindow(std::shared_ptr<saucer::application>& app);
~ToolboxWindow();
bool m_bShow = false;
// Methode zum Anzeigen des Fensters
void show();
void hide();
[[nodiscard]] bool isVisible() const noexcept;
// Für die Fensterinitialisierung
void initialize();
void setupToolboxWindow();
void toggleDevTools();
std::atomic_bool isIndex = true;
std::vector<ToolboxItem> m_toolboxItems;
saucer::smartview m_webview;
@@ -40,20 +38,23 @@ private:
HWND m_hwnd{};
std::shared_ptr<saucer::application> m_app;
std::unique_ptr<optionWindow> m_optionWindow;
std::unique_ptr<LuaHelpWindow> m_luaHelpWindow;
std::thread m_threadMonitor;
std::jthread m_repositionThread;
std::atomic_bool m_isVisible = false;
std::atomic_bool m_isIndex = true;
std::atomic<bool> m_runningMonitor = true;
std::chrono::steady_clock::time_point m_lastShowTime{};
bool m_seenFocusSinceShow = false;
std::atomic_bool m_seenFocusSinceShow = false;
std::shared_ptr<std::atomic_bool> m_alive = std::make_shared<std::atomic_bool>(true);
std::string jsonString;
#ifdef _WIN32
std::filesystem::path userPath = std::getenv("USERPROFILE"); // Windows
#else
std::filesystem::path userPath = std::getenv("HOME"); // Unix/Linux/MacOS
#endif
std::filesystem::path userPath = userDirectory();
static std::filesystem::path userDirectory();
void startThreadMonitor();
void scheduleDelayedReposition();
bool serveHtmlFile(const std::string &htmlPath, bool setDecorations);
@@ -69,8 +70,9 @@ private:
void debug_print();
void run_lua(int id);
void cancel_lua(int id);
LuaRunner::RunResult run_lua(int id);
std::string run_lua_path(const std::string& toolPath);
void cancel_lua(const std::string& toolPath);
void monitor_focus();
@@ -78,10 +80,15 @@ private:
void open_cmd();
void setWindowOpacity(int percent);
int getWindowOpacity() const;
void SetJsonItems();
void openEinst();
void openEinst(int id);
void openEinst(const std::string& toolPath);
void openAddTool();
void openLuaHelp();
void centerWindow();
void resizeWindow(int width, int height);
void resizeWindow(double widthPercent, double heightPercent);
@@ -89,17 +96,19 @@ private:
void openIndex();
void getNewContent();
int getLuaTimeoutSeconds();
void setLuaTimeoutSeconds(int seconds);
std::string getActiveToolName();
std::string getActiveToolDetails() const;
std::string saveActiveToolDetails(const std::string &payload);
std::string openActiveToolFolder() const;
int m_activeToolId = -1;
std::filesystem::path m_activeToolPath;
LuaRunner m_luaRunner;
ToolConfigStore m_configStore;
int m_lastOpacity = 100;
std::filesystem::path getToolConfigPath(int id) const;
void ensureConfigExists(int id) const;
int getLuaTimeoutSecondsForId(int id) const;
std::string getToolConfigIni() const;
void setToolConfigIni(const std::string &content);
std::string createTool(const std::string &payload);
std::string deleteTool(const std::string& toolPath);
optionWindow& optionWindowInstance();
LuaHelpWindow& luaHelpWindowInstance();
};

1
lib/CLI11 Submodule

Submodule lib/CLI11 added at 37bb6edc53

1
lib/rang Submodule

Submodule lib/rang added at 56419fe334

View File

@@ -1,11 +1,40 @@
#include <Windows.h>
#include <saucer/app.hpp>
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "toolboxCli.h"
#include "toolboxWindow.h"
#include "trayController.h"
std::string wideToUtf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (length <= 0) {
return {};
}
std::string result(static_cast<std::size_t>(length), '\0');
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), result.data(), length, nullptr, nullptr);
return result;
}
bool isCliRequest(const std::vector<std::string>& args) {
if (args.empty()) {
return false;
}
const std::string& first = args.front();
return first == "-h" || first == "--help" || first == "help" ||
first == "list" || first == "create" || first == "run" ||
first == "--home" || first == "--color";
}
coco::stray start(saucer::application* app) {
std::shared_ptr<saucer::application> appShared(app, [](saucer::application*) {});
@@ -20,12 +49,18 @@ coco::stray start(saucer::application* app) {
co_await app->finish();
}
#ifdef _DEBUG
int main(int, char**) {
int wmain(int argc, wchar_t** argv) {
std::vector<std::string> args;
args.reserve(static_cast<std::size_t>(std::max(0, argc - 1)));
for (int i = 1; i < argc; ++i) {
args.push_back(wideToUtf8(argv[i]));
}
if (isCliRequest(args)) {
return runCli(std::move(args));
}
// GUI mode: hide the console window that a console-subsystem binary gets.
if (HWND console = GetConsoleWindow(); console != nullptr) {
ShowWindow(console, SW_HIDE);
}
return saucer::application::create({.id = "toolbox"})->run(start);
}
#else
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
return saucer::application::create({.id = "toolbox"})->run(start);
}
#endif

347
res/html/add_tool.html Normal file
View File

@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool erstellen</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
<div id="shell" class="settings-shell">
<div class="resize-handle resize-top" data-webview-resize="t"></div>
<div class="resize-handle resize-right" data-webview-resize="r"></div>
<div class="resize-handle resize-bottom" data-webview-resize="b"></div>
<div class="resize-handle resize-left" data-webview-resize="l"></div>
<div class="resize-handle resize-top-left" data-webview-resize="tl"></div>
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<button class="titlebar-btn" id="back-button" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Neues Tool</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
<button class="titlebar-btn close" data-webview-close data-webview-ignore>×</button>
</div>
</div>
<main class="add-tool-page">
<header class="add-tool-heading">
<div>
<span class="add-tool-eyebrow">Toolbox Builder</span>
<h1>Tool erstellen</h1>
<p>Erstellt einen vollständigen Ordner in <code>.Toolbox</code>.</p>
</div>
<div class="add-tool-icon-preview" id="icon-preview" aria-label="Icon-Vorschau">+</div>
</header>
<form id="add-tool-form" class="add-tool-form">
<section class="add-tool-section add-tool-template-section">
<div class="add-tool-section-heading">
<div>
<h2>Vorlage auswählen</h2>
<p>Ein Ziel auswählen Toolbox erzeugt Name und Startskript automatisch.</p>
</div>
<span class="add-tool-template-badge">Einfach</span>
</div>
<div class="add-tool-template-grid" role="group" aria-label="Tool-Vorlage">
<button type="button" class="add-tool-template" data-template="program">
<span class="add-tool-template-icon"></span>
<span><strong>Programm</strong><small>Eine .exe starten</small></span>
</button>
<button type="button" class="add-tool-template" data-template="folder">
<span class="add-tool-template-icon"></span>
<span><strong>Ordner</strong><small>Im Explorer öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="website">
<span class="add-tool-template-icon"></span>
<span><strong>Website</strong><small>Eine URL öffnen</small></span>
</button>
<button type="button" class="add-tool-template is-active" data-template="lua" aria-pressed="true">
<span class="add-tool-template-icon">λ</span>
<span><strong>Freies Lua</strong><small>Eigenes Skript</small></span>
</button>
</div>
<div id="website-template-controls" class="add-tool-template-controls" hidden>
<input id="website-url" class="settings-input" type="url" autocomplete="url"
placeholder="https://example.com">
<button type="button" id="apply-website-template" class="add-tool-secondary">Übernehmen</button>
</div>
</section>
<section class="add-tool-section">
<h2>Allgemein</h2>
<label for="tool-name">Name <span aria-hidden="true">*</span></label>
<input id="tool-name" class="settings-input" maxlength="80" autocomplete="off" required
placeholder="Zum Beispiel: Projekt öffnen">
<label for="tool-description">Beschreibung</label>
<textarea id="tool-description" class="settings-input add-tool-description" maxlength="8192"
placeholder="Was erledigt dieses Tool?"></textarea>
<div class="add-tool-grid">
<div>
<label for="tool-timeout">Lua-Timeout</label>
<div class="add-tool-input-suffix">
<input id="tool-timeout" class="settings-input" type="number" min="0" max="86400" value="30">
<span>Sek.</span>
</div>
</div>
<div>
<label for="tool-icon">Icon (PNG)</label>
<input id="tool-icon" class="settings-input add-tool-file" type="file" accept="image/png">
</div>
</div>
</section>
<details id="advanced-script" class="add-tool-section add-tool-script-section" open>
<summary>
<span>Erweitert: start.lua bearbeiten</span>
<small>optional</small>
</summary>
<div class="add-tool-section-heading">
<h2>start.lua</h2>
<div class="add-tool-script-meta">
<span>Wird beim Klick auf die Karte ausgeführt</span>
<button type="button" class="lua-help-trigger" id="lua-help-button">Lua-Cheatsheet</button>
</div>
</div>
<div class="add-tool-code-editor">
<pre id="tool-script-highlight" aria-hidden="true"><code class="language-lua"></code></pre>
<textarea id="tool-script" class="settings-input add-tool-script" spellcheck="false" wrap="off"
aria-label="Lua-Skript">-- Dein Toolbox-Skript
print("Tool gestartet")
</textarea>
</div>
</details>
<div class="add-tool-actions">
<div id="create-status" class="settings-status" role="status" aria-live="polite"></div>
<button type="button" id="cancel-button" class="add-tool-secondary">Abbrechen</button>
<button type="submit" id="create-button" class="back-button settings-save">Tool erstellen</button>
</div>
</form>
</main>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
const form = document.getElementById('add-tool-form');
const nameInput = document.getElementById('tool-name');
const descriptionInput = document.getElementById('tool-description');
const timeoutInput = document.getElementById('tool-timeout');
const scriptInput = document.getElementById('tool-script');
const scriptHighlight = document.querySelector('#tool-script-highlight code');
const scriptHighlightContainer = document.getElementById('tool-script-highlight');
const iconInput = document.getElementById('tool-icon');
const iconPreview = document.getElementById('icon-preview');
const status = document.getElementById('create-status');
const createButton = document.getElementById('create-button');
const advancedScript = document.getElementById('advanced-script');
const templateButtons = [...document.querySelectorAll('[data-template]')];
const websiteControls = document.getElementById('website-template-controls');
const websiteUrl = document.getElementById('website-url');
const applyWebsiteButton = document.getElementById('apply-website-template');
let iconBase64 = '';
let templateType = 'lua';
let templateTarget = '';
function highlightLua() {
scriptHighlight.textContent = scriptInput.value + (scriptInput.value.endsWith('\n') ? ' ' : '');
Prism.highlightElement(scriptHighlight);
}
scriptInput.addEventListener('input', () => {
if (templateType !== 'lua') {
selectTemplate('lua');
advancedScript.open = true;
}
highlightLua();
});
scriptInput.addEventListener('scroll', () => {
scriptHighlightContainer.scrollTop = scriptInput.scrollTop;
scriptHighlightContainer.scrollLeft = scriptInput.scrollLeft;
});
function selectTemplate(kind) {
if (kind !== templateType) {
templateTarget = '';
}
templateType = kind;
templateButtons.forEach(button => {
const selected = button.dataset.template === kind;
button.classList.toggle('is-active', selected);
button.setAttribute('aria-pressed', String(selected));
});
websiteControls.hidden = kind !== 'website';
}
function applyTemplateResponse(responseText, kind) {
const response = JSON.parse(responseText);
if (response.cancelled) {
status.textContent = '';
delete status.dataset.kind;
return;
}
if (!response.success || !response.draft) {
status.textContent = response.message || 'Vorlage konnte nicht erstellt werden.';
status.dataset.kind = 'error';
return;
}
nameInput.value = response.draft.name;
descriptionInput.value = response.draft.description;
scriptInput.value = response.draft.luaScript;
templateType = response.draft.templateType;
templateTarget = response.draft.templateTarget;
highlightLua();
advancedScript.open = false;
status.textContent = `${kind} ist bereit. Nur noch „Tool erstellen“ klicken.`;
status.dataset.kind = 'success';
nameInput.focus();
nameInput.select();
}
async function chooseNativeTemplate(kind) {
selectTemplate(kind === 'Programm' ? 'program' : 'folder');
status.textContent = kind === 'Programm' ? 'Programm auswählen…' : 'Ordner auswählen…';
status.dataset.kind = 'pending';
try {
const responseText = kind === 'Programm'
? await saucer.exposed.selectProgramTemplate()
: await saucer.exposed.selectFolderTemplate();
applyTemplateResponse(responseText, kind);
} catch (error) {
console.error('Vorlage konnte nicht erstellt werden:', error);
status.textContent = 'Der Auswahldialog konnte nicht geöffnet werden.';
status.dataset.kind = 'error';
}
}
templateButtons.forEach(button => {
button.addEventListener('click', () => {
const kind = button.dataset.template;
if (kind === 'program') {
void chooseNativeTemplate('Programm');
} else if (kind === 'folder') {
void chooseNativeTemplate('Ordner');
} else if (kind === 'website') {
selectTemplate('website');
websiteUrl.focus();
} else {
selectTemplate('lua');
advancedScript.open = true;
scriptInput.focus();
}
});
});
async function applyWebsiteTemplate() {
selectTemplate('website');
status.textContent = 'Website-Vorlage wird erstellt…';
status.dataset.kind = 'pending';
try {
applyTemplateResponse(
await saucer.exposed.createWebsiteTemplate(websiteUrl.value.trim()),
'Website'
);
} catch (error) {
console.error('Website-Vorlage konnte nicht erstellt werden:', error);
status.textContent = 'Website-Vorlage konnte nicht erstellt werden.';
status.dataset.kind = 'error';
}
}
applyWebsiteButton.addEventListener('click', () => void applyWebsiteTemplate());
websiteUrl.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.preventDefault();
void applyWebsiteTemplate();
}
});
function returnToToolbox() {
saucer.exposed.openIndex();
}
document.getElementById('back-button').addEventListener('click', returnToToolbox);
document.getElementById('cancel-button').addEventListener('click', returnToToolbox);
document.getElementById('lua-help-button').addEventListener('click', () => {
saucer.exposed.openLuaHelp();
});
iconInput.addEventListener('change', () => {
const file = iconInput.files[0];
iconBase64 = '';
iconPreview.replaceChildren('+');
if (!file) {
return;
}
if (file.type !== 'image/png' || file.size > 2 * 1024 * 1024) {
iconInput.value = '';
status.textContent = 'Bitte ein PNG mit maximal 2 MB auswählen.';
status.dataset.kind = 'error';
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const dataUrl = String(reader.result || '');
iconBase64 = dataUrl.includes(',') ? dataUrl.split(',', 2)[1] : '';
const image = document.createElement('img');
image.src = dataUrl;
image.alt = 'Gewähltes Tool-Icon';
iconPreview.replaceChildren(image);
status.textContent = '';
delete status.dataset.kind;
});
reader.readAsDataURL(file);
});
form.addEventListener('submit', async event => {
event.preventDefault();
const name = nameInput.value.trim();
if (!name) {
status.textContent = 'Bitte einen Namen eingeben.';
status.dataset.kind = 'error';
nameInput.focus();
return;
}
createButton.disabled = true;
status.textContent = 'Tool wird erstellt…';
status.dataset.kind = 'pending';
try {
const responseText = await saucer.exposed.createTool(JSON.stringify({
name,
description: descriptionInput.value,
timeoutSeconds: Number.parseInt(timeoutInput.value, 10) || 0,
luaScript: scriptInput.value,
iconBase64,
templateType,
templateTarget,
}));
const response = JSON.parse(responseText);
status.textContent = response.message || (response.success ? 'Tool wurde erstellt.' : 'Tool konnte nicht erstellt werden.');
status.dataset.kind = response.success ? 'success' : 'error';
if (response.success) {
window.setTimeout(returnToToolbox, 700);
return;
}
} catch (error) {
console.error('Tool konnte nicht erstellt werden:', error);
status.textContent = 'Tool konnte nicht erstellt werden.';
status.dataset.kind = 'error';
}
createButton.disabled = false;
});
highlightLua();
nameInput.focus();
</script>
</body>
</html>

View File

@@ -1,165 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
<div id="shell">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
<ul>
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
<li><a href="#" onclick="showContent('none')">Option 3</a></li>
<li><a href="#" onclick="showContent('none')">Option 4</a></li>
</ul>
</div>
<!-- Hauptcontainer -->
<div class="main-container">
<!-- Header mit Zurück-Button -->
<div class="header">
<h1>Einstellungen</h1>
<button class="back-button" onclick="goBack()"></button>
</div>
<!-- Zentraler Editor (Standardmäßig versteckt) -->
<div class="editor" id="editor">
<label for="code-editor"></label>
<textarea id="code-editor" placeholder="Write Lua code here..."></textarea>
<pre><code id="highlightedOutput" class="language-lua"></code></pre>
</div>
<div class="general" id="general">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" class="settings-tool-name">-</div>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
</div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
<script>
function goBack() {
saucer.exposed.openIndex();
}
function showContent(option) {
const general = document.getElementById('general');
const editor = document.getElementById('editor');
if (option === 'editor') {
editor.style.display = 'flex';
general.style.display = 'none';
} else if (option === 'general') {
general.style.display = 'flex';
editor.style.display = 'none';
} else {
editor.style.display = 'none';
general.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('code-editor');
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = highlightLua(editor.value);
});
const toolName = document.getElementById('tool-name');
const timeoutInput = document.getElementById('lua-timeout');
const saveBtn = document.getElementById('save-settings');
const saveStatus = document.getElementById('save-status');
saucer.exposed.getActiveToolName().then(name => {
toolName.textContent = name || '-';
});
saucer.exposed.getLuaTimeoutSeconds().then(value => {
timeoutInput.value = value;
});
saveBtn.addEventListener('click', () => {
const seconds = parseInt(timeoutInput.value, 10);
saucer.exposed.setLuaTimeoutSeconds(Number.isFinite(seconds) ? seconds : 0);
saveStatus.textContent = 'Gespeichert';
setTimeout(() => {
saveStatus.textContent = '';
}, 1500);
});
});
</script>
</body>
</html>

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<title>Tool bearbeiten</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
@@ -16,10 +16,11 @@
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<button class="titlebar-btn" onclick="goBack()" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Einstellungen</span>
<button class="titlebar-btn" id="back-button" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Tool bearbeiten</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
@@ -27,149 +28,445 @@
</div>
</div>
<div class="settings-body">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
<ul>
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
<li><a href="#" onclick="showContent('none')">Option 3</a></li>
<li><a href="#" onclick="showContent('none')">Option 4</a></li>
</ul>
</div>
<!-- Hauptcontainer -->
<div class="main-container">
<!-- Zentraler Editor (Standardmäßig versteckt) -->
<div class="editor" id="editor">
<label for="code-editor"></label>
<textarea id="code-editor" placeholder="Write Lua code here..."></textarea>
<pre><code id="highlightedOutput" class="language-lua"></code></pre>
<main class="tool-settings-page">
<header class="tool-settings-heading">
<div id="settings-icon-preview" class="add-tool-icon-preview" aria-label="Tool-Icon">λ</div>
<div>
<span class="add-tool-eyebrow">Toolbox</span>
<h1 id="settings-heading">Tool bearbeiten</h1>
<p>Eigenschaften und Startverhalten dieses Tools.</p>
</div>
</header>
<div class="general" id="general">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" class="settings-tool-name">-</div>
<nav class="tool-settings-tabs" aria-label="Einstellungsbereiche">
<button type="button" class="tool-settings-tab is-active" data-panel="general">Allgemein</button>
<button type="button" class="tool-settings-tab" data-panel="launch">Startverhalten</button>
<button type="button" class="tool-settings-tab" data-panel="actions">Aktionen</button>
</nav>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
<form id="tool-settings-form" class="tool-settings-form">
<section class="tool-settings-panel is-active" data-panel-content="general">
<div class="add-tool-section">
<h2>Allgemein</h2>
<label for="tool-name">Name</label>
<input id="tool-name" class="settings-input" maxlength="80" autocomplete="off" required>
<label for="tool-description">Beschreibung</label>
<textarea id="tool-description" class="settings-input add-tool-description" maxlength="8192"
placeholder="Was erledigt dieses Tool?"></textarea>
<div class="add-tool-grid">
<div>
<label for="tool-timeout">Lua-Timeout</label>
<div class="add-tool-input-suffix">
<input id="tool-timeout" class="settings-input" type="number" min="0" max="86400" value="30">
<span>Sek.</span>
</div>
<small class="tool-settings-help">0 deaktiviert den Timeout.</small>
</div>
<div>
<label for="tool-icon">Icon (PNG)</label>
<input id="tool-icon" class="settings-input add-tool-file" type="file" accept="image/png">
<button type="button" id="remove-icon" class="tool-settings-link">Icon entfernen</button>
</div>
</div>
</div>
</section>
<section class="tool-settings-panel" data-panel-content="launch">
<div class="add-tool-section add-tool-template-section">
<div class="add-tool-section-heading">
<div>
<h2>Starttyp</h2>
<p>Standardvorlagen bleiben ohne Lua-Kenntnisse bearbeitbar.</p>
</div>
<span id="template-badge" class="add-tool-template-badge">Lua</span>
</div>
<div class="add-tool-template-grid" role="group" aria-label="Starttyp">
<button type="button" class="add-tool-template" data-template="program">
<span class="add-tool-template-icon"></span>
<span><strong>Programm</strong><small>Eine .exe starten</small></span>
</button>
<button type="button" class="add-tool-template" data-template="folder">
<span class="add-tool-template-icon"></span>
<span><strong>Ordner</strong><small>Im Explorer öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="website">
<span class="add-tool-template-icon"></span>
<span><strong>Website</strong><small>Eine URL öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="lua">
<span class="add-tool-template-icon">λ</span>
<span><strong>Freies Lua</strong><small>Eigenes Skript</small></span>
</button>
</div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
<div id="native-target-controls" class="tool-settings-target" hidden>
<label for="template-target">Ausgewähltes Ziel</label>
<div class="add-tool-template-controls">
<input id="template-target" class="settings-input" readonly>
<button type="button" id="select-target" class="add-tool-secondary">Neu auswählen</button>
</div>
</div>
<div id="website-target-controls" class="tool-settings-target" hidden>
<label for="website-url">Website-Adresse</label>
<div class="add-tool-template-controls">
<input id="website-url" class="settings-input" type="url" autocomplete="url"
placeholder="https://example.com">
<button type="button" id="apply-website" class="add-tool-secondary">Übernehmen</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
<div id="lua-section" class="add-tool-section">
<div class="add-tool-section-heading">
<h2>start.lua</h2>
<button type="button" class="lua-help-trigger" id="lua-help-button">Lua-Cheatsheet</button>
</div>
<p class="tool-settings-help">Wird beim Klick auf die Toolkarte ausgeführt.</p>
<div class="add-tool-code-editor tool-settings-editor">
<pre id="tool-script-highlight" aria-hidden="true"><code class="language-lua"></code></pre>
<textarea id="tool-script" class="settings-input add-tool-script" spellcheck="false" wrap="off"
aria-label="Lua-Skript"></textarea>
</div>
</div>
<div id="generated-script-note" class="add-tool-section tool-settings-generated" hidden>
<span class="tool-settings-generated-icon"></span>
<div>
<h2>Startskript wird automatisch gepflegt</h2>
<p>Toolbox erzeugt <code>start.lua</code> beim Speichern sicher aus dem ausgewählten Ziel.</p>
</div>
</div>
</section>
<section class="tool-settings-panel" data-panel-content="actions">
<div class="add-tool-section tool-settings-action-card">
<div>
<h2>Toolordner</h2>
<p>Öffnet den Ordner mit <code>start.lua</code>, <code>config.ini</code> und weiteren Tooldateien.</p>
</div>
<button type="button" id="open-tool-folder" class="add-tool-secondary">Im Explorer öffnen</button>
</div>
<div class="add-tool-section tool-settings-action-card">
<div>
<h2 style="color:#ff6b6b">Tool löschen</h2>
<p>Entfernt dieses Tool unwiderruflich inklusive aller Dateien.</p>
</div>
<button type="button" id="delete-tool" class="add-tool-secondary" style="color:#ff6b6b;border-color:rgba(255,107,107,.4)">Löschen…</button>
</div>
</section>
</form>
</main>
<footer class="tool-settings-footer">
<div id="settings-status" class="settings-status" role="status" aria-live="polite"></div>
<button type="button" id="cancel-button" class="add-tool-secondary">Zurück</button>
<button type="submit" form="tool-settings-form" id="save-button" class="back-button settings-save">Speichern</button>
</footer>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
function goBack() {
const form = document.getElementById('tool-settings-form');
const nameInput = document.getElementById('tool-name');
const descriptionInput = document.getElementById('tool-description');
const timeoutInput = document.getElementById('tool-timeout');
const iconInput = document.getElementById('tool-icon');
const iconPreview = document.getElementById('settings-icon-preview');
const heading = document.getElementById('settings-heading');
const scriptInput = document.getElementById('tool-script');
const scriptHighlight = document.querySelector('#tool-script-highlight code');
const scriptHighlightContainer = document.getElementById('tool-script-highlight');
const nativeTargetControls = document.getElementById('native-target-controls');
const websiteTargetControls = document.getElementById('website-target-controls');
const targetInput = document.getElementById('template-target');
const websiteInput = document.getElementById('website-url');
const luaSection = document.getElementById('lua-section');
const generatedNote = document.getElementById('generated-script-note');
const templateBadge = document.getElementById('template-badge');
const templateButtons = [...document.querySelectorAll('[data-template]')];
const status = document.getElementById('settings-status');
const saveButton = document.getElementById('save-button');
let templateType = 'lua';
let templateTarget = '';
let iconBase64 = '';
let iconChanged = false;
function setStatus(message = '', kind = '') {
status.textContent = message;
if (kind) {
status.dataset.kind = kind;
} else {
delete status.dataset.kind;
}
}
function returnToToolbox() {
saucer.exposed.openIndex();
}
function showContent(option) {
const general = document.getElementById('general');
const editor = document.getElementById('editor');
function highlightLua() {
scriptHighlight.textContent = scriptInput.value + (scriptInput.value.endsWith('\n') ? ' ' : '');
Prism.highlightElement(scriptHighlight);
}
if (option === 'editor') {
editor.style.display = 'flex';
general.style.display = 'none';
} else if (option === 'general') {
general.style.display = 'flex';
editor.style.display = 'none';
} else {
editor.style.display = 'none';
general.style.display = 'none';
function renderIcon() {
iconPreview.replaceChildren();
if (!iconBase64) {
iconPreview.textContent = 'λ';
return;
}
const image = document.createElement('img');
image.src = `data:image/png;base64,${iconBase64}`;
image.alt = 'Tool-Icon';
iconPreview.appendChild(image);
}
function selectTemplate(kind) {
templateType = kind;
templateButtons.forEach(button => {
const selected = button.dataset.template === kind;
button.classList.toggle('is-active', selected);
button.setAttribute('aria-pressed', String(selected));
});
const labels = {program: 'Programm', folder: 'Ordner', website: 'Website', lua: 'Lua'};
templateBadge.textContent = labels[kind] || 'Lua';
nativeTargetControls.hidden = kind !== 'program' && kind !== 'folder';
websiteTargetControls.hidden = kind !== 'website';
luaSection.hidden = kind !== 'lua';
generatedNote.hidden = kind === 'lua';
targetInput.value = kind === 'program' || kind === 'folder' ? templateTarget : '';
websiteInput.value = kind === 'website' ? templateTarget : '';
}
function applyTemplateResponse(responseText) {
const response = JSON.parse(responseText);
if (response.cancelled) {
setStatus();
return false;
}
if (!response.success || !response.draft) {
setStatus(response.message || 'Das Ziel konnte nicht übernommen werden.', 'error');
return false;
}
templateType = response.draft.templateType;
templateTarget = response.draft.templateTarget;
scriptInput.value = response.draft.luaScript;
selectTemplate(templateType);
highlightLua();
setStatus('Startziel wurde übernommen. Zum Anwenden noch speichern.', 'success');
return true;
}
async function chooseNativeTarget(kind) {
setStatus(kind === 'program' ? 'Programm auswählen…' : 'Ordner auswählen…', 'pending');
try {
const response = kind === 'program'
? await saucer.exposed.selectProgramTemplate()
: await saucer.exposed.selectFolderTemplate();
applyTemplateResponse(response);
} catch (error) {
console.error('Zielauswahl fehlgeschlagen:', error);
setStatus('Der Auswahldialog konnte nicht geöffnet werden.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('code-editor');
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
async function applyWebsite() {
setStatus('Website wird geprüft…', 'pending');
try {
applyTemplateResponse(await saucer.exposed.createWebsiteTemplate(websiteInput.value.trim()));
} catch (error) {
console.error('Website-Prüfung fehlgeschlagen:', error);
setStatus('Die Website konnte nicht übernommen werden.', 'error');
}
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = highlightLua(editor.value);
function showPanel(name) {
document.querySelectorAll('[data-panel-content]').forEach(panel => {
panel.classList.toggle('is-active', panel.dataset.panelContent === name);
});
const toolName = document.getElementById('tool-name');
const timeoutInput = document.getElementById('lua-timeout');
const saveBtn = document.getElementById('save-settings');
const saveStatus = document.getElementById('save-status');
saucer.exposed.getActiveToolName().then(name => {
toolName.textContent = name || '-';
});
saucer.exposed.getLuaTimeoutSeconds().then(value => {
timeoutInput.value = value;
});
saveBtn.addEventListener('click', () => {
const seconds = parseInt(timeoutInput.value, 10);
saucer.exposed.setLuaTimeoutSeconds(Number.isFinite(seconds) ? seconds : 0);
saveStatus.textContent = 'Gespeichert';
setTimeout(() => {
saveStatus.textContent = '';
}, 1500);
document.querySelectorAll('[data-panel]').forEach(tab => {
tab.classList.toggle('is-active', tab.dataset.panel === name);
});
}
document.querySelectorAll('[data-panel]').forEach(tab => {
tab.addEventListener('click', () => showPanel(tab.dataset.panel));
});
</script>
templateButtons.forEach(button => {
button.addEventListener('click', () => {
const kind = button.dataset.template;
if (kind === 'program' || kind === 'folder') {
void chooseNativeTarget(kind);
return;
}
if (kind === 'website') {
templateTarget = '';
selectTemplate('website');
websiteInput.focus();
return;
}
templateTarget = '';
selectTemplate('lua');
scriptInput.focus();
});
});
document.getElementById('select-target').addEventListener('click', () => void chooseNativeTarget(templateType));
document.getElementById('apply-website').addEventListener('click', () => void applyWebsite());
websiteInput.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.preventDefault();
void applyWebsite();
}
});
scriptInput.addEventListener('input', highlightLua);
scriptInput.addEventListener('scroll', () => {
scriptHighlightContainer.scrollTop = scriptInput.scrollTop;
scriptHighlightContainer.scrollLeft = scriptInput.scrollLeft;
});
iconInput.addEventListener('change', () => {
const file = iconInput.files[0];
if (!file) {
return;
}
if (file.type !== 'image/png' || file.size > 2 * 1024 * 1024) {
iconInput.value = '';
setStatus('Bitte ein PNG mit maximal 2 MB auswählen.', 'error');
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const dataUrl = String(reader.result || '');
iconBase64 = dataUrl.includes(',') ? dataUrl.split(',', 2)[1] : '';
iconChanged = true;
renderIcon();
setStatus('Neues Icon ausgewählt. Zum Anwenden noch speichern.', 'success');
});
reader.readAsDataURL(file);
});
document.getElementById('remove-icon').addEventListener('click', () => {
iconBase64 = '';
iconChanged = true;
iconInput.value = '';
renderIcon();
setStatus('Icon wird beim Speichern entfernt.', 'success');
});
document.getElementById('open-tool-folder').addEventListener('click', async () => {
try {
const response = JSON.parse(await saucer.exposed.openToolFolder());
setStatus(response.message, response.success ? 'success' : 'error');
} catch (error) {
console.error('Toolordner konnte nicht geöffnet werden:', error);
setStatus('Der Toolordner konnte nicht geöffnet werden.', 'error');
}
});
document.getElementById('lua-help-button').addEventListener('click', () => saucer.exposed.openLuaHelp());
document.getElementById('delete-tool')?.addEventListener('click', async () => {
const toolName = nameInput.value.trim() || 'dieses Tool';
if (!confirm(`"${toolName}" unwiderruflich löschen?`)) {
return;
}
try {
const details = JSON.parse(await saucer.exposed.getToolDetails());
const toolPath = details.success ? (details.details?.path || details.path || '') : '';
if (!toolPath) {
setStatus('Tool-Pfad konnte nicht ermittelt werden.', 'error');
return;
}
const response = JSON.parse(await saucer.exposed.deleteTool(toolPath));
if (response.success) {
returnToToolbox();
} else {
setStatus(response.message || 'Löschen fehlgeschlagen.', 'error');
}
} catch (error) {
console.error('Löschen fehlgeschlagen:', error);
setStatus('Löschen fehlgeschlagen.', 'error');
}
});
document.getElementById('back-button').addEventListener('click', returnToToolbox);
document.getElementById('cancel-button').addEventListener('click', returnToToolbox);
form.addEventListener('submit', async event => {
event.preventDefault();
if (!nameInput.value.trim()) {
showPanel('general');
nameInput.focus();
setStatus('Bitte einen Toolnamen eingeben.', 'error');
return;
}
if (templateType === 'website') {
templateTarget = websiteInput.value.trim();
}
saveButton.disabled = true;
setStatus('Änderungen werden gespeichert…', 'pending');
const payload = {
name: nameInput.value.trim(),
description: descriptionInput.value,
timeoutSeconds: Number.parseInt(timeoutInput.value, 10) || 0,
luaScript: scriptInput.value,
templateType,
templateTarget,
};
if (iconChanged) {
payload.iconBase64 = iconBase64;
}
try {
const response = JSON.parse(await saucer.exposed.saveToolDetails(JSON.stringify(payload)));
setStatus(response.message || (response.success ? 'Tool wurde gespeichert.' : 'Speichern fehlgeschlagen.'),
response.success ? 'success' : 'error');
if (response.success) {
heading.textContent = nameInput.value.trim();
iconChanged = false;
}
} catch (error) {
console.error('Tool konnte nicht gespeichert werden:', error);
setStatus('Tool konnte nicht gespeichert werden.', 'error');
}
saveButton.disabled = false;
});
async function loadTool() {
setStatus('Tool wird geladen…', 'pending');
try {
const response = JSON.parse(await saucer.exposed.getToolDetails());
if (!response.success || !response.details) {
setStatus(response.message || 'Tool konnte nicht geladen werden.', 'error');
saveButton.disabled = true;
return;
}
const details = response.details;
nameInput.value = details.name || '';
descriptionInput.value = details.description || '';
timeoutInput.value = details.timeoutSeconds ?? 30;
scriptInput.value = details.luaScript || '';
templateType = details.templateType || 'lua';
templateTarget = details.templateTarget || '';
iconBase64 = details.iconBase64 || '';
iconChanged = false;
heading.textContent = details.name || 'Tool bearbeiten';
renderIcon();
selectTemplate(templateType);
highlightLua();
setStatus();
} catch (error) {
console.error('Tool konnte nicht geladen werden:', error);
setStatus('Tool konnte nicht geladen werden.', 'error');
saveButton.disabled = true;
}
}
void loadTool();
</script>
</body>
</html>

View File

@@ -20,6 +20,7 @@
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">&#x1f4c1;</button>
</div>
<button onclick="openEinstFirst()" class="button icon-button" id="option-button" style="display: none;">&#9881;</button>
<button onclick="toggleGeneralSettings()" class="button icon-button" id="general-settings-button" title="Einstellungen">&#9881;</button>
</div>
</div>
<div class="main" id="toolbox">
@@ -32,36 +33,54 @@
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
<div id="settings-overlay" class="settings-overlay" hidden>
<div class="settings-panel">
<div class="settings-panel-header">
<h2>Einstellungen</h2>
<button type="button" class="settings-panel-close" onclick="toggleGeneralSettings()">&times;</button>
</div>
<div class="settings-panel-body">
<div class="settings-row">
<div class="settings-row-label">
<span class="settings-row-title">Fenster-Deckkraft</span>
<span class="settings-row-desc">70% bis 100% bei 100% ist das Fenster vollstaendig deckend.</span>
</div>
<div class="settings-row-control">
<input type="range" id="opacity-slider" min="70" max="100" value="100" step="1">
<span id="opacity-value" class="settings-row-value">100%</span>
</div>
</div>
</div>
</div>
</div>
<script src="saucer://embedded/html/tool-runtime.js"></script>
<script>
function getNewContent(){
saucer.exposed.getNewContent();
}
function runLuaWithId(event) {
async function runLuaWithId(event) {
event.preventDefault();
if (event.target.tagName === 'BUTTON') {
// Falls der Button gedrückt wurde, brich die Funktion ab
if (event.target.closest('button')) {
return false;
}
const button = event.target;
const toolCard = button.closest('.tool-card');
const toolCard = event.currentTarget;
if (toolCard) {
const toolId = toolCard.getAttribute('data-tool-id');
const toolKey = toolCard.dataset.toolKey;
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) !== 'true') {
console.log(`Disabling button for tool ID: ${toolId}`);
saucer.exposed.run_lua(parseInt(toolId, 10));
// Button deaktivieren
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
// Speichern des Zustands im sessionStorage
sessionStorage.setItem(`toolButtonDisabled_${toolId}`, 'true');
} else {
if (ToolRuntime.isCardRunBlocked(toolCard)) {
console.log(`Button is already disabled for tool ID: ${toolId}`);
return false;
}
try {
const response = JSON.parse(await saucer.exposed.run_lua(toolKey));
if (response.singleInstance === true) {
await loadToolboxItems();
}
} catch (error) {
console.error('Fehler beim Starten des Tools:', error);
}
}
}
@@ -79,83 +98,93 @@
}
function openAddTool() {
openFileSystem();
saucer.exposed.openAddTool();
}
function createToolCard(item) {
const toolId = String(item.id);
const card = document.createElement('div');
card.className = 'tool-card';
card.dataset.url = String(item.url || '');
card.dataset.toolId = toolId;
card.dataset.toolKey = String(item.path || '');
card.addEventListener('click', runLuaWithId);
card.addEventListener('contextmenu', showContextMenu);
const icon = document.createElement('img');
const iconData = typeof item.icon === 'string' ? item.icon : '';
icon.src = iconData.startsWith('data:image/png;base64,')
? iconData
: `data:image/png;base64,${iconData}`;
icon.alt = String(item.name || '');
const info = document.createElement('div');
info.className = 'tool-info';
const name = document.createElement('h3');
name.textContent = String(item.name || '');
const description = document.createElement('p');
description.textContent = String(item.description || '');
const action = document.createElement('div');
action.className = 'tool-action';
const settingsButton = document.createElement('button');
settingsButton.type = 'button';
settingsButton.textContent = '⚙';
settingsButton.title = 'Tool bearbeiten';
settingsButton.setAttribute('aria-label', 'Tool bearbeiten');
settingsButton.addEventListener('click', event => {
openEinst(event, card.dataset.toolKey);
});
const runningStatus = document.createElement('span');
runningStatus.className = 'tool-running-status';
runningStatus.textContent = 'Läuft…';
runningStatus.hidden = true;
info.append(name, description);
action.append(runningStatus, settingsButton);
card.append(icon, info, action);
ToolRuntime.applyCardState(card, ToolRuntime.initialRunning(item));
return card;
}
function renderToolboxItems(items) {
const toolbox = document.getElementById('toolbox');
toolbox.querySelectorAll('.tool-card').forEach(card => card.remove());
const cards = document.createDocumentFragment();
items.forEach(item => cards.appendChild(createToolCard(item)));
toolbox.insertBefore(cards, toolbox.querySelector('.add-tool-card'));
}
function loadToolboxItems() {
return saucer.exposed.getToolboxItems()
.then(jsonString => {
const items = JSON.parse(jsonString);
renderToolboxItems(Array.isArray(items) ? items : []);
})
.catch(error => console.error('Fehler beim Abrufen der Toolbox-Elemente:', error));
}
function reloadContent() {
const toolbox = document.getElementById('toolbox');
// Toolbox leeren, aber das add-tool-card Element behalten
const addToolCard = toolbox.querySelector('.add-tool-card');
toolbox.innerHTML = '';
// Toolbox-Daten neu abrufen
saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString);
m_toolboxItems.forEach(item => {
const iconSrc = item.icon.startsWith('data:image/png;base64,')
? item.icon
: `data:image/png;base64,${item.icon}`;
const toolCardHTML = `
<div class="tool-card" data-url="${item.url}"
data-tool-id="${item.id}"
onclick="runLuaWithId(event)">
<img src="${iconSrc}" alt="${item.name}">
<div class="tool-info">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
<div class="tool-action">
<button onclick="openEinst(event, ${item.id})">Einst</button>
</div>
</div>
`;
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands der Buttons
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');
const button = card.querySelector('button');
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) === 'true') {
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
}
});
}).catch(error => console.error('Fehler beim Abrufen der Toolbox-Elemente:', error));
loadToolboxItems();
}
function enableButtonByToolId(toolId) {
saucer.exposed.debug_print();
// Finden Sie den Button mit dem entsprechenden data-tool-id
const button = document.querySelector(`.tool-card[data-tool-id='${toolId}']`);
if (button) {
button.disabled = false;
button.classList.remove('running');
button.textContent = 'Einst';
// Speichern des Zustands im sessionStorage
sessionStorage.setItem(`toolButtonDisabled_${toolId}`, 'false');
} else {
console.error('Button mit Tool-ID ' + toolId + ' nicht gefunden.');
}
function setToolRunningByPath(toolKey, running) {
const card = Array.from(document.querySelectorAll('.tool-card'))
.find(candidate => candidate.dataset.toolKey === toolKey);
ToolRuntime.applyCardState(card, running);
}
function openEinst(event, toolId){
function openEinst(event, toolPath){
if (event) {
event.stopPropagation();
event.preventDefault();
}
console.log('Einst Button geklickt!');
saucer.exposed.openEinst(toolId);
saucer.exposed.openEinst(toolPath);
}
function openEinstFirst(){
@@ -163,11 +192,100 @@
if (!firstCard) {
return;
}
const toolId = firstCard.getAttribute('data-tool-id');
saucer.exposed.openEinst(parseInt(toolId));
saucer.exposed.openEinst(firstCard.dataset.toolKey);
}
let activeContextMenu = null;
function closeContextMenu() {
if (activeContextMenu) {
activeContextMenu.remove();
activeContextMenu = null;
}
}
function showContextMenu(event) {
event.preventDefault();
event.stopPropagation();
closeContextMenu();
const card = event.currentTarget;
const toolKey = card.dataset.toolKey;
const toolName = card.querySelector('h3')?.textContent || 'dieses Tool';
const menu = document.createElement('div');
menu.className = 'context-menu';
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.textContent = 'Bearbeiten';
editBtn.addEventListener('click', () => {
closeContextMenu();
openEinst(null, toolKey);
});
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.textContent = 'Löschen';
deleteBtn.className = 'context-menu-danger';
deleteBtn.addEventListener('click', () => {
closeContextMenu();
confirmDeleteTool(toolKey, toolName);
});
menu.append(editBtn, deleteBtn);
document.body.append(menu);
activeContextMenu = menu;
const mx = Math.min(event.clientX, window.innerWidth - 180);
const my = Math.min(event.clientY, window.innerHeight - 90);
menu.style.left = mx + 'px';
menu.style.top = my + 'px';
}
async function confirmDeleteTool(toolKey, toolName) {
if (!confirm(`"${toolName}" wirklich löschen?`)) {
return;
}
try {
const response = JSON.parse(await saucer.exposed.deleteTool(toolKey));
if (response.success) {
await loadToolboxItems();
} else {
alert(response.message || 'Löschen fehlgeschlagen.');
}
} catch (error) {
console.error('Fehler beim Löschen:', error);
alert('Löschen fehlgeschlagen.');
}
}
document.addEventListener('click', closeContextMenu);
let settingsOverlayVisible = false;
let currentOpacity = 100;
function toggleGeneralSettings() {
const overlay = document.getElementById('settings-overlay');
settingsOverlayVisible = !settingsOverlayVisible;
overlay.hidden = !settingsOverlayVisible;
if (settingsOverlayVisible) {
const slider = document.getElementById('opacity-slider');
const valueLabel = document.getElementById('opacity-value');
slider.value = currentOpacity;
valueLabel.textContent = currentOpacity + '%';
}
}
document.addEventListener('DOMContentLoaded', () => {
const slider = document.getElementById('opacity-slider');
const valueLabel = document.getElementById('opacity-value');
slider.addEventListener('input', () => {
const val = parseInt(slider.value, 10);
valueLabel.textContent = val + '%';
currentOpacity = val;
document.getElementById('shell').style.opacity = (val / 100).toString();
});
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link) {
@@ -175,53 +293,7 @@
}
}, true);
const toolbox = document.getElementById('toolbox');
// Das add-tool-card Element entfernen und später wieder hinzufügen
const addToolCard = toolbox.querySelector('.add-tool-card');
saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString);
m_toolboxItems.forEach(item => {
const iconSrc = item.icon.startsWith('data:image/png;base64,')
? item.icon
: `data:image/png;base64,${item.icon}`;
const toolCardHTML = `
<div class="tool-card" data-url="${item.url}"
data-tool-id="${item.id}"
onclick="runLuaWithId(event)">
<img src="${iconSrc}" alt="${item.name}">
<div class="tool-info">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
<div class="tool-action">
<button onclick="openEinst(event, ${item.id})">Einst</button>
</div>
</div>
`;
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands beim Laden der Seite
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');
const button = card.querySelector('button');
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) === 'true') {
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
}
});
}).catch(error => console.error('Error fetching toolbox items:', error));
loadToolboxItems();
});
</script>
</body>

View File

@@ -0,0 +1,347 @@
/<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lua-Cheatsheet</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page lua-help-page">
<div id="shell" class="settings-shell">
<div class="resize-handle resize-top" data-webview-resize="t"></div>
<div class="resize-handle resize-right" data-webview-resize="r"></div>
<div class="resize-handle resize-bottom" data-webview-resize="b"></div>
<div class="resize-handle resize-left" data-webview-resize="l"></div>
<div class="resize-handle resize-top-left" data-webview-resize="tl"></div>
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<span class="lua-help-mark" aria-hidden="true">λ</span>
<span class="titlebar-title" data-webview-drag>Lua-Hilfe</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
<button class="titlebar-btn close" data-webview-close data-webview-ignore>×</button>
</div>
</div>
<nav class="lua-help-tabs" aria-label="Lua-Hilfe" role="tablist">
<button class="lua-help-tab active" id="cheatsheet-tab" type="button" role="tab"
aria-selected="true" aria-controls="cheatsheet-panel">Cheatsheet</button>
<button class="lua-help-tab" id="official-docs-tab" type="button" role="tab"
aria-selected="false">
Offizielle Lua-5.4-Doku
</button>
<span class="lua-help-doc-status" id="docs-status" role="status" aria-live="polite">Online · lua.org</span>
</nav>
<div class="lua-help-panels">
<main class="lua-help-content" id="cheatsheet-panel" role="tabpanel" aria-labelledby="cheatsheet-tab">
<header class="lua-help-hero">
<div>
<span class="add-tool-eyebrow">Toolbox · Lua 5.4.1</span>
<h1>Lua auf einen Blick</h1>
<p>Die wichtigsten Muster für <code>start.lua</code> lokal verfügbar, kurz und kopierbar.</p>
</div>
<div class="lua-help-badge">start.lua</div>
</header>
<section class="lua-help-notice">
<strong>Toolbox-Kontext</strong>
<span>Das Skript startet beim Klick auf eine Tool-Karte. Timeout <code>0</code> bedeutet unbegrenzt. Tools sind vertrauenswürdig und dürfen über <code>os</code> und <code>io</code> auf das System zugreifen.</span>
</section>
<div class="lua-help-grid">
<article class="lua-help-card">
<h2>Nur eine laufende Instanz</h2>
<pre><code class="language-lua">-- toolbox: single-instance
-- Während dieses Skript läuft, wird seine
-- Tool-Karte rot angezeigt und gesperrt.
-- Ohne die Direktive sind parallele Starts erlaubt.</code></pre>
</article>
<article class="lua-help-card">
<h2>Variablen &amp; Typen</h2>
<pre><code class="language-lua">local name = "Toolbox"
local count = 3
local enabled = true
local nothing = nil
-- Typen prüfen
type(name) -- "string"
tostring(count) -- "3"
tonumber("42") -- 42</code></pre>
</article>
<article class="lua-help-card">
<h2>Operatoren</h2>
<pre><code class="language-lua">-- Rechnen: + - * / // % ^
local half = 7 // 2 -- 3
local text = "Lua " .. "5.4"
local length = #text
-- Vergleich: == ~= &lt; &gt; &lt;= &gt;=
-- Logik: and, or, not</code></pre>
</article>
<article class="lua-help-card">
<h2>Bedingungen</h2>
<pre><code class="language-lua">if count &gt; 10 then
print("groß")
elseif count &gt; 0 then
print("positiv")
else
print("leer")
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Schleifen</h2>
<pre><code class="language-lua">for i = 1, 5 do
print(i)
end
local i = 1
while i &lt;= 3 do
i = i + 1
end
repeat
i = i - 1
until i == 0</code></pre>
</article>
<article class="lua-help-card">
<h2>Funktionen</h2>
<pre><code class="language-lua">local function greet(name)
return "Hallo " .. name
end
local function position()
return 10, 20
end
local x, y = position()
print(greet("Toolbox"), x, y)</code></pre>
</article>
<article class="lua-help-card">
<h2>Tabellen</h2>
<pre><code class="language-lua">local tool = {
name = "Backup",
tags = { "files", "daily" }
}
print(tool.name)
print(tool.tags[1]) -- ab 1 indiziert
for key, value in pairs(tool) do
print(key, value)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Strings</h2>
<pre><code class="language-lua">local path = [[C:\Projekte\Toolbox]]
local upper = string.upper("lua")
local found = string.find("start.lua", "lua")
local formatted = string.format("%s: %d", "Dateien", 4)</code></pre>
</article>
<article class="lua-help-card">
<h2>Dateien</h2>
<pre><code class="language-lua">local file, err = io.open("status.txt", "w")
if not file then
error(err)
end
file:write("fertig\n")
file:close()</code></pre>
</article>
<article class="lua-help-card">
<h2>Fehler behandeln</h2>
<pre><code class="language-lua">local ok, result = pcall(function()
assert(false, "etwas ging schief")
end)
if not ok then
print("Fehler:", result)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Module</h2>
<pre><code class="language-lua">-- helper.lua
local M = {}
function M.run()
return "fertig"
end
return M
-- start.lua
local helper = require("helper")
print(helper.run())</code></pre>
</article>
<article class="lua-help-card">
<h2>Gültigkeitsbereiche</h2>
<pre><code class="language-lua">local outer = "sichtbar"
do
local inner = "nur im Block"
print(outer, inner)
end
-- Ohne local wird eine globale Variable erzeugt.
-- Globale Namen möglichst vermeiden.</code></pre>
</article>
<article class="lua-help-card">
<h2>Listen &amp; <code>ipairs</code></h2>
<pre><code class="language-lua">local files = { "a.txt", "b.txt" }
table.insert(files, "c.txt")
local last = table.remove(files)
for index, file in ipairs(files) do
print(index, file)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Mehrfachwerte &amp; Varargs</h2>
<pre><code class="language-lua">local function bounds()
return 10, 20
end
local function join(...)
return table.concat({ ... }, ", ")
end
local min, max = bounds()
print(join("Lua", min, max))</code></pre>
</article>
<article class="lua-help-card">
<h2>String-Muster</h2>
<pre><code class="language-lua">local text = "build-2026.log"
local year = text:match("build%-(%d+)%.log")
local clean = text:gsub("[^%w%.%-]", "_")
for word in string.gmatch("a,b,c", "[^,]+") do
print(word)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Zeit &amp; Datum</h2>
<pre><code class="language-lua">local now = os.time()
local stamp = os.date("%Y-%m-%d %H:%M:%S", now)
print(stamp)
local started = os.clock()
-- Arbeit ...
print("CPU-Sekunden", os.clock() - started)</code></pre>
</article>
<article class="lua-help-card">
<h2>Coroutinen</h2>
<pre><code class="language-lua">local worker = coroutine.create(function()
coroutine.yield("Schritt 1")
return "fertig"
end)
local ok, value = coroutine.resume(worker)
assert(ok, value)
print(value)</code></pre>
</article>
<article class="lua-help-card">
<h2>Metatables</h2>
<pre><code class="language-lua">local defaults = { timeout = 30 }
local config = setmetatable({}, {
__index = defaults
})
print(config.timeout) -- 30
config.timeout = 10</code></pre>
</article>
<article class="lua-help-card">
<h2>Defensiv aufräumen</h2>
<pre><code class="language-lua">local file &lt;close&gt; = assert(io.open("data.txt", "r"))
local content = file:read("*a")
-- Lua 5.4 schließt file beim Verlassen
-- des Gültigkeitsbereichs automatisch.
assert(#content &gt; 0, "Datei ist leer")</code></pre>
</article>
</div>
<section class="lua-help-libraries">
<h2>In Toolbox verfügbare Bibliotheken</h2>
<div class="lua-help-chips">
<code>base</code><code>os</code><code>string</code><code>coroutine</code><code>package</code>
<code>table</code><code>math</code><code>utf8</code><code>io</code>
</div>
</section>
<section class="lua-help-libraries">
<h2>Toolbox-Rezepte unter Windows</h2>
<div class="lua-help-grid">
<article class="lua-help-card">
<h2>Programm starten</h2>
<pre><code class="language-lua">-- start benötigt einen leeren Fenstertitel vor dem Pfad
local command = [[start "" "C:\Program Files\App\app.exe"]]
local ok, kind, code = os.execute(command)
assert(ok, string.format("Start fehlgeschlagen: %s %s", kind, code))</code></pre>
</article>
<article class="lua-help-card">
<h2>Ordner öffnen</h2>
<pre><code class="language-lua">local folder = [[C:\Projekte]]
os.execute('explorer "' .. folder .. '"')
-- Relative Pfade beziehen sich auf das
-- Arbeitsverzeichnis von Toolbox.</code></pre>
</article>
<article class="lua-help-card">
<h2>Umgebungsvariablen</h2>
<pre><code class="language-lua">local profile = assert(os.getenv("USERPROFILE"))
local temp = os.getenv("TEMP") or profile
print(profile, temp)</code></pre>
</article>
<article class="lua-help-card">
<h2>Sicheres Kommando-Quoting</h2>
<pre><code class="language-lua">local function quote(value)
assert(not value:find('"'), "Ungültiges Anführungszeichen")
return '"' .. value .. '"'
end
-- Keine ungeprüften Eingaben an os.execute übergeben.</code></pre>
</article>
</div>
</section>
<section class="lua-help-notice lua-help-footer-note">
<strong>Timeout &amp; Vertrauen</strong>
<span>Toolbox prüft den Timeout zwischen Lua-Instruktionen. Blockierende native Aufrufe wie <code>os.execute()</code> können erst nach ihrer Rückkehr unterbrochen werden. Führe daher nur überprüfte Skripte und Programme aus.</span>
</section>
</main>
</div>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
const docsTab = document.getElementById('official-docs-tab');
docsTab.addEventListener('click', () => {
saucer.exposed.showOfficialLuaDocs();
});
Prism.highlightAll();
</script>
</body>
</html>

View File

@@ -15,6 +15,32 @@
--accent: #4fc3ff;
--accent-deep: #1e74c9;
}
/* Globale Scrollbar-Stile im Toolbox-Design */
* {
scrollbar-width: thin;
scrollbar-color: rgba(60, 150, 220, 0.95) rgba(20, 45, 70, 0.4);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: rgba(20, 45, 70, 0.3);
border-radius: 999px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(120, 210, 255, 0.9), rgba(60, 150, 220, 0.95));
border-radius: 999px;
border: 2px solid rgba(20, 45, 70, 0.4);
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(210, 245, 255, 0.85), rgba(120, 210, 255, 0.9));
}
::-webkit-scrollbar-corner {
background: transparent;
}
html {
background: transparent;
border-radius: 18px;
@@ -33,7 +59,7 @@
display: flex;
flex-direction: column;
height: 100vh;
background: rgba(10, 26, 40, 0.80);
background: rgba(10, 26, 40, 1.0);
border-radius: 18px;
overflow: hidden;
}
@@ -127,20 +153,6 @@
flex: 1;
padding: 16px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.main::-webkit-scrollbar {
width: 10px;
}
.main::-webkit-scrollbar-track {
background: rgba(20, 45, 70, 0.45);
border-radius: 999px;
}
.main::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(120, 210, 255, 0.8), rgba(60, 150, 220, 0.9));
border-radius: 999px;
border: 2px solid rgba(20, 45, 70, 0.4);
}
.tool-card {
display: flex;
@@ -161,6 +173,14 @@
background: var(--glass-strong);
box-shadow: 0 16px 26px var(--shadow), inset 0 1px 0 var(--shine);
}
.tool-card.tool-running,
.tool-card.tool-running:hover {
transform: none;
cursor: not-allowed;
background: linear-gradient(135deg, rgba(125, 24, 35, 0.82), rgba(68, 18, 29, 0.9));
border-color: rgba(255, 102, 112, 0.85);
box-shadow: 0 12px 24px rgba(75, 5, 15, 0.48), inset 0 1px 0 rgba(255, 190, 195, 0.18);
}
.tool-card img {
width: 62px;
height: 62px;
@@ -185,8 +205,16 @@
color: #9bc4dc;
}
.tool-action {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.tool-running-status {
color: #ffb7bd;
font-size: 11px;
font-weight: 650;
}
.tool-action button {
background: linear-gradient(180deg, rgba(130, 220, 255, 0.9), rgba(40, 120, 190, 0.95));
border: 1px solid rgba(140, 210, 255, 0.6);
@@ -521,3 +549,899 @@
.settings-page .settings-save {
margin-top: 16px;
}
.add-tool-page {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 22px;
position: relative;
z-index: 1;
}
.add-tool-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.add-tool-heading h1 {
margin: 3px 0 5px;
color: var(--ink);
font-size: 24px;
}
.add-tool-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
}
.add-tool-heading code {
color: var(--accent);
}
.add-tool-eyebrow {
color: var(--accent);
font-size: 11px;
font-weight: 700;
letter-spacing: 1.4px;
text-transform: uppercase;
}
.add-tool-icon-preview {
width: 58px;
height: 58px;
flex: 0 0 58px;
display: grid;
place-items: center;
overflow: hidden;
border: 1px solid var(--glass-border);
border-radius: 16px;
background: var(--glass-strong);
color: var(--accent);
font-size: 28px;
box-shadow: 0 10px 20px var(--shadow), inset 0 1px 0 var(--shine);
}
.add-tool-icon-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.add-tool-form {
display: grid;
gap: 14px;
}
.add-tool-section {
padding: 16px;
border: 1px solid var(--glass-border);
border-radius: 14px;
background: var(--glass);
box-shadow: 0 12px 22px var(--shadow), inset 0 1px 0 var(--shine);
}
.add-tool-section h2 {
margin: 0 0 14px;
color: var(--ink);
font-size: 15px;
}
.add-tool-section label {
display: block;
margin: 11px 0 6px;
color: var(--ink-soft);
font-size: 12px;
font-weight: 600;
}
.add-tool-description {
min-height: 64px;
resize: vertical;
font-family: inherit;
}
.add-tool-grid {
display: grid;
grid-template-columns: minmax(130px, 0.7fr) minmax(180px, 1.3fr);
gap: 14px;
}
.add-tool-input-suffix {
display: flex;
align-items: center;
gap: 8px;
}
.add-tool-input-suffix span {
color: var(--ink-soft);
font-size: 12px;
}
.add-tool-file {
padding: 7px;
font-size: 12px;
}
.add-tool-section-heading {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.add-tool-section-heading span {
color: var(--ink-soft);
font-size: 11px;
}
.add-tool-template-section .add-tool-section-heading {
align-items: flex-start;
}
.add-tool-template-section .add-tool-section-heading h2 {
margin-bottom: 4px;
}
.add-tool-template-section .add-tool-section-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
}
.add-tool-template-badge {
flex: 0 0 auto;
padding: 5px 9px;
border: 1px solid rgba(120, 225, 190, 0.38);
border-radius: 999px;
background: rgba(70, 190, 145, 0.14);
color: #91efc5 !important;
font-weight: 700;
}
.add-tool-template-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
margin-top: 14px;
}
.add-tool-template {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
padding: 10px;
border: 1px solid var(--glass-border);
border-radius: 11px;
background: rgba(12, 28, 45, 0.48);
color: var(--ink);
text-align: left;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
}
.add-tool-template:hover {
border-color: rgba(140, 210, 255, 0.48);
background: rgba(28, 60, 86, 0.58);
transform: translateY(-1px);
}
.add-tool-template.is-active {
border-color: rgba(120, 210, 255, 0.68);
background: linear-gradient(180deg, rgba(65, 150, 210, 0.28), rgba(30, 80, 120, 0.32));
box-shadow: inset 0 1px 0 var(--shine);
}
.add-tool-template-icon {
width: 30px;
height: 30px;
flex: 0 0 30px;
display: grid;
place-items: center;
border-radius: 9px;
background: rgba(75, 180, 235, 0.14);
color: var(--accent);
font-size: 15px;
}
.add-tool-template strong,
.add-tool-template small {
display: block;
}
.add-tool-template strong {
margin-bottom: 2px;
font-size: 12px;
}
.add-tool-template small {
overflow: hidden;
color: var(--ink-soft);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.add-tool-template-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.add-tool-template-controls[hidden] {
display: none;
}
.add-tool-template-controls .settings-input {
margin: 0;
}
.add-tool-script-section > summary {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--ink);
font-size: 13px;
font-weight: 700;
cursor: pointer;
list-style: none;
}
.add-tool-script-section > summary::-webkit-details-marker {
display: none;
}
.add-tool-script-section > summary::before {
content: "";
margin-right: 8px;
color: var(--accent);
font-size: 18px;
transition: transform 0.16s ease;
}
.add-tool-script-section[open] > summary::before {
transform: rotate(90deg);
}
.add-tool-script-section > summary small {
margin-left: auto;
color: var(--ink-soft);
font-size: 10px;
font-weight: 500;
}
.add-tool-script-section[open] > .add-tool-section-heading {
margin-top: 16px;
}
.add-tool-script-meta {
display: flex;
align-items: center;
gap: 10px;
}
.lua-help-trigger {
border: 1px solid rgba(120, 210, 255, 0.42);
border-radius: 999px;
padding: 5px 10px;
background: rgba(50, 135, 200, 0.24);
color: #aee6ff;
font: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, transform 0.16s ease;
}
.lua-help-trigger:hover {
background: rgba(70, 165, 230, 0.38);
transform: translateY(-1px);
}
.add-tool-code-editor {
position: relative;
min-height: 150px;
border: 1px solid var(--glass-border);
border-radius: 10px;
background: rgba(12, 28, 45, 0.6);
box-shadow: inset 0 1px 0 var(--shine);
overflow: hidden;
}
.add-tool-code-editor pre,
.settings-page .add-tool-script {
width: 100%;
min-height: 150px;
margin: 0;
padding: 12px;
border: 0;
border-radius: 0;
box-sizing: border-box;
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.5;
tab-size: 4;
white-space: pre;
}
.add-tool-code-editor pre {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
color: var(--ink);
}
.settings-page .add-tool-script {
position: relative;
z-index: 2;
resize: vertical;
overflow: auto;
background: transparent;
color: transparent;
caret-color: var(--ink);
box-shadow: none;
}
.settings-page .add-tool-script::selection {
background: rgba(90, 175, 235, 0.35);
color: transparent;
}
.add-tool-code-editor .token.comment {
color: #7897a8;
font-style: italic;
}
.add-tool-code-editor .token.string {
color: #ffd27a;
}
.add-tool-code-editor .token.number,
.add-tool-code-editor .token.boolean {
color: #8fe7ff;
}
.add-tool-code-editor .token.keyword {
color: #88bfff;
font-weight: 600;
}
.add-tool-code-editor .token.function {
color: #8be0c5;
}
.add-tool-code-editor .token.operator,
.add-tool-code-editor .token.punctuation {
color: #b9d5e5;
}
.add-tool-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
min-height: 38px;
}
.add-tool-actions .settings-status {
margin: 0 auto 0 2px;
}
.add-tool-actions .settings-save {
margin: 0;
}
.add-tool-secondary {
border: 1px solid var(--glass-border);
border-radius: 999px;
padding: 8px 14px;
background: rgba(12, 28, 45, 0.6);
color: var(--ink-soft);
cursor: pointer;
}
.settings-status[data-kind="success"] {
color: #83e8b2;
}
.settings-status[data-kind="error"] {
color: #ff9a9a;
}
.settings-status[data-kind="pending"] {
color: var(--accent);
}
.lua-help-mark {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 1px solid rgba(140, 210, 255, 0.38);
border-radius: 8px;
background: rgba(79, 195, 255, 0.14);
color: #8fe7ff;
font: 700 18px/1 Georgia, serif;
}
.lua-help-tabs {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 6px;
padding: 9px 16px 0;
border-bottom: 1px solid rgba(140, 210, 255, 0.18);
background: rgba(10, 26, 40, 0.68);
}
.lua-help-tab {
position: relative;
bottom: -1px;
border: 1px solid transparent;
border-bottom: 0;
border-radius: 10px 10px 0 0;
padding: 9px 14px 10px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-size: 12px;
font-weight: 650;
cursor: pointer;
}
.lua-help-tab.active {
border-color: rgba(140, 210, 255, 0.28);
background: rgba(19, 43, 63, 0.96);
color: var(--ink);
}
.lua-help-tab:hover {
color: #8fe7ff;
background: rgba(79, 195, 255, 0.1);
}
.lua-help-doc-status {
margin-left: auto;
padding-right: 4px;
color: #8fe7ff;
font-size: 11px;
}
.lua-help-panels {
display: flex;
flex: 1;
min-height: 0;
}
.lua-help-content {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 22px;
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.lua-help-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 16px;
}
.lua-help-hero h1 {
margin: 4px 0 6px;
font-size: 27px;
color: var(--ink);
}
.lua-help-hero p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
}
.lua-help-hero p code {
color: #8fe7ff;
}
.lua-help-badge {
border: 1px solid rgba(140, 210, 255, 0.36);
border-radius: 12px;
padding: 10px 14px;
background: rgba(79, 195, 255, 0.1);
color: #9fe2ff;
font: 700 12px/1 "Consolas", monospace;
box-shadow: inset 0 1px 0 var(--shine);
}
.lua-help-notice {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: baseline;
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid rgba(255, 210, 122, 0.24);
border-radius: 12px;
background: rgba(95, 72, 25, 0.16);
color: var(--ink-soft);
font-size: 12px;
}
.lua-help-notice strong {
color: #ffd27a;
}
.lua-help-notice code {
color: #8fe7ff;
}
.lua-help-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 13px;
}
.lua-help-card,
.lua-help-libraries {
min-width: 0;
border: 1px solid var(--glass-border);
border-radius: 14px;
background: var(--glass);
box-shadow: 0 10px 20px rgba(4, 10, 16, 0.32), inset 0 1px 0 var(--shine);
}
.lua-help-card {
padding: 14px;
}
.lua-help-card h2,
.lua-help-libraries h2 {
margin: 0 0 10px;
color: #dff6ff;
font-size: 14px;
}
.lua-help-card pre {
margin: 0;
overflow-x: auto;
white-space: pre;
border: 0;
background: transparent;
color: var(--ink);
font: 12px/1.55 "Consolas", "Courier New", monospace;
tab-size: 4;
}
.lua-help-card .token.comment {
color: #7897a8;
font-style: italic;
}
.lua-help-card .token.string {
color: #ffd27a;
}
.lua-help-card .token.number,
.lua-help-card .token.boolean {
color: #8fe7ff;
}
.lua-help-card .token.keyword {
color: #88bfff;
font-weight: 650;
}
.lua-help-card .token.function {
color: #8be0c5;
}
.lua-help-card .token.operator,
.lua-help-card .token.punctuation {
color: #b9d5e5;
}
.lua-help-libraries {
margin-top: 14px;
padding: 14px;
}
.lua-help-chips {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.lua-help-chips code {
border: 1px solid rgba(140, 210, 255, 0.24);
border-radius: 999px;
padding: 5px 9px;
background: rgba(79, 195, 255, 0.1);
color: #aee6ff;
font-size: 11px;
}
.lua-help-footer-note {
margin: 14px 0 0;
}
@media (max-width: 720px) {
.lua-help-grid {
grid-template-columns: 1fr;
}
.lua-help-doc-status,
.lua-help-tab.active {
display: none;
}
.add-tool-script-meta {
align-items: flex-end;
flex-direction: column;
gap: 6px;
}
}
#create-button:disabled {
cursor: wait;
opacity: 0.65;
}
.tool-settings-page {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 20px 22px;
position: relative;
z-index: 1;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.tool-settings-heading {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 16px;
}
.tool-settings-heading h1 {
margin: 3px 0 4px;
overflow: hidden;
color: var(--ink);
font-size: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-settings-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
}
.tool-settings-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 14px;
padding: 5px;
border: 1px solid rgba(140, 210, 255, 0.22);
border-radius: 12px;
background: rgba(8, 22, 34, 0.46);
}
.tool-settings-tab {
border: 1px solid transparent;
border-radius: 9px;
padding: 8px 7px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-size: 11px;
font-weight: 650;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
.tool-settings-tab:hover {
background: rgba(79, 195, 255, 0.1);
color: var(--ink);
}
.tool-settings-tab.is-active {
border-color: rgba(140, 210, 255, 0.38);
background: linear-gradient(180deg, rgba(65, 150, 210, 0.28), rgba(30, 80, 120, 0.32));
color: var(--ink);
box-shadow: inset 0 1px 0 var(--shine);
}
.tool-settings-form,
.tool-settings-panel {
min-width: 0;
}
.tool-settings-panel:not(.is-active) {
display: none;
}
.tool-settings-panel.is-active {
display: grid;
gap: 12px;
}
.tool-settings-help {
display: block;
margin: 6px 0 10px;
color: var(--ink-soft);
font-size: 11px;
}
.tool-settings-link {
margin-top: 7px;
padding: 0;
border: 0;
background: transparent;
color: #8fd7ff;
font: inherit;
font-size: 11px;
cursor: pointer;
}
.tool-settings-link:hover {
color: #d2f5ff;
text-decoration: underline;
}
.tool-settings-target {
margin-top: 12px;
}
.tool-settings-target > label {
display: block;
margin-bottom: 6px;
color: var(--ink-soft);
font-size: 12px;
font-weight: 600;
}
.tool-settings-target .add-tool-secondary {
flex: 0 0 auto;
}
.tool-settings-editor {
min-height: 225px;
}
.tool-settings-editor pre,
.tool-settings-editor .add-tool-script {
min-height: 225px;
}
.tool-settings-generated {
display: flex;
align-items: flex-start;
gap: 12px;
}
.tool-settings-generated-icon {
width: 30px;
height: 30px;
flex: 0 0 30px;
display: grid;
place-items: center;
border: 1px solid rgba(120, 225, 190, 0.38);
border-radius: 50%;
background: rgba(70, 190, 145, 0.14);
color: #91efc5;
font-weight: 800;
}
.tool-settings-generated h2,
.tool-settings-action-card h2 {
margin-bottom: 5px;
}
.tool-settings-generated p,
.tool-settings-action-card p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.5;
}
.tool-settings-generated code,
.tool-settings-action-card code {
color: var(--accent);
}
.tool-settings-action-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.tool-settings-action-card .add-tool-secondary {
flex: 0 0 auto;
}
.tool-settings-footer {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 10px;
min-height: 38px;
padding: 10px 14px;
border-top: 1px solid rgba(140, 210, 255, 0.2);
background: linear-gradient(180deg, rgba(16, 36, 54, 0.92), rgba(8, 20, 31, 0.94));
box-shadow: 0 -8px 20px rgba(4, 10, 16, 0.28);
}
.tool-settings-footer .settings-status {
flex: 1;
margin: 0;
}
.tool-settings-footer .settings-save {
margin: 0;
}
#save-button:disabled {
cursor: wait;
opacity: 0.65;
}
/* Kontextmenü */
.context-menu {
position: fixed;
z-index: 9999;
min-width: 160px;
padding: 6px 0;
border-radius: 10px;
background: rgba(15, 30, 50, 0.98);
border: 1px solid rgba(140, 210, 255, 0.22);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(12px);
}
.context-menu button {
display: block;
width: 100%;
padding: 8px 16px;
border: none;
background: transparent;
color: #dff6ff;
font: 13px/1.3 "Segoe UI", sans-serif;
text-align: left;
cursor: pointer;
transition: background 0.12s ease;
}
.context-menu button:hover {
background: rgba(40, 80, 120, 0.6);
}
.context-menu .context-menu-danger {
color: #ff8a80;
}
.context-menu .context-menu-danger:hover {
background: rgba(180, 40, 40, 0.4);
}
/* Allgemeine Einstellungen Overlay */
.settings-overlay {
position: fixed;
inset: 0;
z-index: 9000;
background: rgba(4, 10, 16, 0.55);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
}
.settings-overlay[hidden] {
display: none;
}
.settings-panel {
width: 340px;
max-height: 70vh;
overflow-y: auto;
background: linear-gradient(180deg, rgba(18, 40, 60, 0.97), rgba(12, 28, 45, 0.98));
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.settings-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid rgba(140, 210, 255, 0.15);
}
.settings-panel-header h2 {
margin: 0;
font-size: 16px;
color: var(--ink);
}
.settings-panel-close {
background: none;
border: none;
color: var(--ink-soft);
font-size: 22px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.settings-panel-close:hover {
color: var(--accent);
}
.settings-panel-body {
padding: 16px 20px;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.settings-row-label {
flex: 1;
}
.settings-row-title {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--ink);
margin-bottom: 2px;
}
.settings-row-desc {
display: block;
font-size: 11px;
color: var(--ink-soft);
opacity: 0.7;
line-height: 1.3;
}
.settings-row-control {
display: flex;
align-items: center;
gap: 8px;
}
.settings-row-value {
font-size: 12px;
font-weight: 600;
color: var(--accent);
min-width: 38px;
text-align: right;
}
.settings-row-control input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100px;
height: 6px;
border-radius: 999px;
background: rgba(20, 45, 70, 0.6);
outline: none;
}
.settings-row-control input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(180deg, var(--aqua-2), var(--aqua-3));
border: 2px solid rgba(20, 45, 70, 0.5);
cursor: pointer;
}
.settings-row-control input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--aqua-3);
border: 2px solid rgba(20, 45, 70, 0.5);
cursor: pointer;
}

33
res/html/tool-runtime.js Normal file
View File

@@ -0,0 +1,33 @@
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) {
module.exports = api;
}
root.ToolRuntime = api;
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
function initialRunning(item) {
return item?.singleInstance === true && item?.running === true;
}
function isCardRunBlocked(card) {
return card?.dataset?.runDisabled === 'true';
}
function applyCardState(card, running) {
if (!card) {
return;
}
const active = running === true;
card.classList.toggle('tool-running', active);
card.dataset.runDisabled = active ? 'true' : 'false';
card.setAttribute('aria-busy', active ? 'true' : 'false');
const status = card.querySelector('.tool-running-status');
if (status) {
status.hidden = !active;
}
}
return { initialRunning, isCardRunBlocked, applyCardState };
}));

21
res/vendor/prism/LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT LICENSE
Copyright (c) 2012 Lea Verou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
res/vendor/prism/prism-core.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
res/vendor/prism/prism-lua.min.js vendored Normal file
View File

@@ -0,0 +1 @@
Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/};

308
src/luaHelpWindow.cpp Normal file
View File

@@ -0,0 +1,308 @@
#include "luaHelpWindow.h"
#include <saucer/embedded/all.hpp>
#include <saucer/navigation.hpp>
#include <saucer/script.hpp>
#include <algorithm>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <utility>
#include <windows.h>
namespace {
constexpr auto kLuaManualUrl = "https://www.lua.org/manual/5.4/";
}
LuaHelpWindow::LuaHelpWindow(std::shared_ptr<saucer::application> app)
: m_app(std::move(app))
, m_webview([this]() -> saucer::smartview {
auto window = saucer::window::create(m_app.get());
if (!window.has_value()) {
throw std::runtime_error("LuaHelpWindow: failed to create saucer::window");
}
saucer::webview::options options{window.value()};
options.storage_path = std::filesystem::temp_directory_path() / "toolbox-lua-help-webview2";
options.browser_flags.insert("--disable-gpu");
auto webview = saucer::smartview::create(options);
if (!webview.has_value()) {
throw std::runtime_error("LuaHelpWindow: failed to create saucer::smartview");
}
return std::move(webview.value());
}())
{
}
void LuaHelpWindow::initialize() {
m_hwndModule = std::make_unique<hwnd_module>(&m_webview);
m_hwnd = m_hwndModule->get_hwnd();
configureWindow();
exposeToJs();
injectDocumentationToolbar();
loadResources();
#ifdef _DEBUG
m_webview.set_dev_tools(false);
#endif
m_webview.on<saucer::webview::event::navigate>([](const saucer::navigation& navigation) {
const auto scheme = navigation.url().scheme();
const auto host = navigation.url().host();
if ((scheme == "saucer" && host && *host == "embedded") || scheme == "about") {
return saucer::policy::allow;
}
if (scheme == "https" && host && *host == "www.lua.org") {
const auto path = navigation.url().path().generic_string();
if (path.starts_with("/manual/5.4")) {
return saucer::policy::allow;
}
}
return saucer::policy::block;
});
m_webview.parent().on<saucer::window::event::close>([this]() -> saucer::policy {
m_webview.parent().hide();
return saucer::policy::block;
});
}
void LuaHelpWindow::show() {
m_webview.serve("/html/lua_cheatsheet.html");
configureWindow();
m_webview.parent().show();
m_webview.parent().focus();
}
void LuaHelpWindow::configureWindow() {
const int screenWidth = GetSystemMetrics(SM_CXSCREEN);
const int screenHeight = GetSystemMetrics(SM_CYSCREEN);
RECT workArea{};
int workLeft = 0;
int workTop = 0;
int workWidth = screenWidth;
int workHeight = screenHeight;
if (SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0)) {
workLeft = static_cast<int>(workArea.left);
workTop = static_cast<int>(workArea.top);
workWidth = static_cast<int>(workArea.right - workArea.left);
workHeight = static_cast<int>(workArea.bottom - workArea.top);
}
const int width = std::min(980, static_cast<int>(workWidth * 0.84));
const int height = std::min(820, static_cast<int>(workHeight * 0.86));
m_webview.parent().set_size({width, height});
const saucer::color background{0x0b, 0x1c, 0x2c, 0x00};
m_webview.set_background(background);
m_webview.parent().set_background(background);
m_webview.parent().set_decorations(saucer::window::decoration::none);
if (m_hwnd) {
SetWindowPos(m_hwnd, nullptr,
workLeft + (workWidth - width) / 2,
workTop + (workHeight - height) / 2,
0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
}
void LuaHelpWindow::loadResources() {
auto resources = saucer::embedded::all();
if (!resources.contains("/html/lua_cheatsheet.html")) {
throw std::runtime_error("LuaHelpWindow: lua_cheatsheet.html is not embedded");
}
m_webview.embed(resources);
m_webview.set_url("saucer://embedded/html/lua_cheatsheet.html");
m_webview.set_context_menu(false);
if (const auto icon = resources.find("/ico/toolbox.png"); icon != resources.end()) {
m_webview.parent().set_icon(saucer::icon::from(icon->second.content).value());
}
m_webview.parent().set_title("Toolbox - Lua-Hilfe");
}
void LuaHelpWindow::exposeToJs() {
m_webview.expose("showLuaCheatsheet", [this]() {
showCheatsheet();
});
m_webview.expose("showOfficialLuaDocs", [this]() {
showOfficialDocumentation();
});
m_webview.expose("luaDocsBack", [this]() {
m_webview.back();
});
m_webview.expose("luaDocsForward", [this]() {
m_webview.forward();
});
m_webview.expose("reloadLuaDocs", [this]() {
m_webview.reload();
});
}
void LuaHelpWindow::injectDocumentationToolbar() {
const std::string script = R"JS((() => {
if (location.hostname !== 'www.lua.org' || !location.pathname.startsWith('/manual/5.4')) {
return;
}
if (document.getElementById('toolbox-lua-doc-toolbar')) {
return;
}
const style = document.createElement('style');
style.textContent = `
html { scroll-padding-top: 112px; }
body { padding-top: 104px !important; }
#toolbox-lua-doc-toolbar {
position: fixed; inset: 0 0 auto 0; z-index: 2147483647;
height: 104px; box-sizing: border-box; display: flex;
flex-direction: column; background: #0d2234; color: #dff6ff;
font: 12px/1.2 "Segoe UI", sans-serif;
box-shadow: 0 5px 18px rgba(0, 0, 0, .28);
}
#toolbox-lua-doc-toolbar .toolbox-doc-titlebar {
height: 52px; box-sizing: border-box; display: flex;
align-items: center; justify-content: space-between;
padding: 10px 12px; border-bottom: 1px solid rgba(140, 210, 255, .2);
background: linear-gradient(180deg, rgba(20, 45, 70, .96), rgba(12, 28, 45, .94));
}
#toolbox-lua-doc-toolbar .toolbox-doc-title-left,
#toolbox-lua-doc-toolbar .toolbox-doc-window-actions,
#toolbox-lua-doc-toolbar .toolbox-doc-tabs {
display: flex; align-items: center;
}
#toolbox-lua-doc-toolbar .toolbox-doc-title-left { gap: 10px; }
#toolbox-lua-doc-toolbar .toolbox-doc-window-actions { gap: 8px; }
#toolbox-lua-doc-toolbar .toolbox-doc-mark {
width: 26px; height: 26px; display: grid; place-items: center;
border: 1px solid rgba(140, 210, 255, .38); border-radius: 8px;
background: rgba(79, 195, 255, .14); color: #8fe7ff;
font: 700 18px/1 Georgia, serif;
}
#toolbox-lua-doc-toolbar .toolbox-doc-title {
color: #b8d3e3; font-size: 14px; letter-spacing: .4px;
}
#toolbox-lua-doc-toolbar .toolbox-doc-window-button,
#toolbox-lua-doc-toolbar .toolbox-doc-nav-button {
border: 1px solid rgba(140, 210, 255, .35); border-radius: 8px;
background: rgba(10, 26, 40, .7); color: #dff6ff;
font: inherit; cursor: pointer;
}
#toolbox-lua-doc-toolbar .toolbox-doc-window-button {
width: 30px; height: 26px; padding: 0;
}
#toolbox-lua-doc-toolbar .toolbox-doc-tabs {
height: 52px; box-sizing: border-box; gap: 6px; padding: 9px 10px 0;
border-bottom: 1px solid rgba(140, 210, 255, .18);
background: rgba(10, 26, 40, .92);
}
#toolbox-lua-doc-toolbar .toolbox-doc-tab {
align-self: stretch; position: relative; bottom: -1px;
border: 1px solid transparent; border-bottom: 0;
border-radius: 10px 10px 0 0; padding: 0 14px;
background: transparent; color: #b8d3e3;
font: inherit; font-weight: 650; cursor: pointer;
}
#toolbox-lua-doc-toolbar .toolbox-doc-active {
border-color: rgba(140, 210, 255, .28);
background: rgba(19, 43, 63, .96); color: #dff6ff;
}
#toolbox-lua-doc-toolbar .toolbox-doc-spacer { flex: 1; }
#toolbox-lua-doc-toolbar .toolbox-doc-origin { color: #8fdcff; }
#toolbox-lua-doc-toolbar .toolbox-doc-nav-button {
width: 30px; height: 28px; padding: 0;
}
#toolbox-lua-doc-toolbar button:hover { background: rgba(30, 65, 95, .9); }
`;
document.head.append(style);
const toolbar = document.createElement('div');
toolbar.id = 'toolbox-lua-doc-toolbar';
const addButton = (parent, label, title, action, className = '') => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.title = title;
button.className = className;
button.setAttribute('data-webview-ignore', '');
if (action) {
button.addEventListener('click', action);
}
parent.append(button);
return button;
};
const titlebar = document.createElement('div');
titlebar.className = 'toolbox-doc-titlebar';
titlebar.setAttribute('data-webview-drag', '');
const titleLeft = document.createElement('div');
titleLeft.className = 'toolbox-doc-title-left';
titleLeft.setAttribute('data-webview-drag', '');
const mark = document.createElement('span');
mark.className = 'toolbox-doc-mark';
mark.textContent = 'λ';
mark.setAttribute('data-webview-drag', '');
const title = document.createElement('span');
title.className = 'toolbox-doc-title';
title.textContent = 'Lua-Hilfe';
title.setAttribute('data-webview-drag', '');
titleLeft.append(mark, title);
const windowActions = document.createElement('div');
windowActions.className = 'toolbox-doc-window-actions';
const minimize = addButton(windowActions, '', 'Minimieren', null, 'toolbox-doc-window-button');
minimize.setAttribute('data-webview-minimize', '');
const close = addButton(windowActions, '×', 'Schließen', null, 'toolbox-doc-window-button');
close.setAttribute('data-webview-close', '');
titlebar.append(titleLeft, windowActions);
const tabs = document.createElement('nav');
tabs.className = 'toolbox-doc-tabs';
tabs.setAttribute('aria-label', 'Lua-Hilfe');
addButton(tabs, 'Cheatsheet', 'Zum Toolbox-Cheatsheet',
() => saucer.exposed.showLuaCheatsheet(), 'toolbox-doc-tab');
addButton(tabs, 'Offizielle Lua-5.4-Doku', 'Startseite des Lua-Handbuchs',
() => saucer.exposed.showOfficialLuaDocs(), 'toolbox-doc-tab toolbox-doc-active');
const spacer = document.createElement('span');
spacer.className = 'toolbox-doc-spacer';
tabs.append(spacer);
const origin = document.createElement('span');
origin.className = 'toolbox-doc-origin';
origin.textContent = 'Online · lua.org';
tabs.append(origin);
addButton(tabs, '', 'Zurück', () => saucer.exposed.luaDocsBack(), 'toolbox-doc-nav-button');
addButton(tabs, '', 'Vor', () => saucer.exposed.luaDocsForward(), 'toolbox-doc-nav-button');
addButton(tabs, '', 'Neu laden', () => saucer.exposed.reloadLuaDocs(), 'toolbox-doc-nav-button');
toolbar.append(titlebar, tabs);
document.body.prepend(toolbar);
})())JS";
m_webview.inject({
.code = script,
.run_at = saucer::script::time::ready,
.no_frames = true,
.clearable = false,
});
m_webview.on<saucer::webview::event::navigated>([this, script](const saucer::url& url) {
const auto host = url.host();
if (url.scheme() == "https" && host && *host == "www.lua.org" &&
url.path().generic_string().starts_with("/manual/5.4")) {
m_webview.saucer::webview::execute(script);
}
});
}
void LuaHelpWindow::showCheatsheet() {
m_webview.serve("/html/lua_cheatsheet.html");
}
void LuaHelpWindow::showOfficialDocumentation() {
m_webview.set_url(kLuaManualUrl);
}

View File

@@ -1,9 +1,14 @@
#include "luaRunner.h"
#include "toolLaunchBridge.h"
#include <chrono>
#include <fstream>
#include <iostream>
#include <ranges>
#include <sstream>
#include <thread>
#include <sol/sol.hpp>
extern "C" {
@@ -21,7 +26,7 @@ struct LuaHookContext {
};
void luaCancelHook(lua_State* hookState, lua_Debug*) {
auto* hookCtx = *reinterpret_cast<LuaHookContext**>(lua_getextraspace(hookState));
auto* hookCtx = *static_cast<LuaHookContext**>(lua_getextraspace(hookState));
if (!hookCtx) {
return;
}
@@ -43,17 +48,71 @@ std::chrono::steady_clock::time_point deadlineFromTimeout(int timeoutSeconds) {
return std::chrono::steady_clock::now() + std::chrono::seconds(timeoutSeconds);
}
std::string pathUtf8(const std::filesystem::path& path) {
const auto encoded = path.u8string();
return {reinterpret_cast<const char*>(encoded.data()), encoded.size()};
}
std::string readScript(const std::filesystem::path& scriptPath) {
std::ifstream input(scriptPath, std::ios::binary);
if (!input) {
throw std::runtime_error("Das Lua-Skript konnte nicht geoeffnet werden: " + pathUtf8(scriptPath));
}
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
bool requestsSingleInstance(std::string_view script) {
std::istringstream lines{std::string(script)};
std::string line;
while (std::getline(lines, line)) {
const auto first = line.find_first_not_of(" \t\r");
const auto last = line.find_last_not_of(" \t\r");
if (first != std::string::npos &&
line.substr(first, last - first + 1) == "-- toolbox: single-instance") {
return true;
}
}
return false;
}
} // namespace
void LuaRunner::run(int id, const std::filesystem::path& scriptPath, int timeoutSeconds, CompletionCallback onComplete) {
auto cancelFlag = std::make_shared<std::atomic_bool>(false);
LuaRunner::~LuaRunner() {
shutdown();
}
LuaRunner::RunResult LuaRunner::run(const std::string& taskId,
const std::filesystem::path& scriptPath,
int timeoutSeconds, CompletionCallback onComplete) {
const std::string script = readScript(scriptPath);
const bool singleInstance = requestsSingleInstance(script);
auto state = m_state;
auto cancelFlag = std::make_shared<std::atomic_bool>(false);
auto executionFinishedFlag = std::make_shared<std::atomic_bool>(false);
auto callbackFinishedFlag = std::make_shared<std::atomic_bool>(false);
std::vector<LuaTask> completedTasks;
{
std::lock_guard<std::mutex> lock(state->tasksMutex);
state->tasks[id] = LuaTask{cancelFlag};
}
auto& tasks = state->tasks[taskId];
for (auto task = tasks.begin(); task != tasks.end();) {
if (task->callbackFinished->load(std::memory_order_acquire)) {
completedTasks.push_back(std::move(*task));
task = tasks.erase(task);
} else {
++task;
}
}
const bool activeSingleInstance = std::ranges::any_of(tasks, &LuaTask::singleInstance);
if (!tasks.empty() && (singleInstance || activeSingleInstance)) {
return {false, true};
}
std::thread([state, id, scriptPath, timeoutSeconds, cancelFlag, onComplete = std::move(onComplete)]() mutable {
tasks.emplace_back(cancelFlag, executionFinishedFlag, callbackFinishedFlag, singleInstance);
tasks.back().worker = std::jthread([
scriptPath, script, timeoutSeconds, cancelFlag, executionFinishedFlag,
callbackFinishedFlag, onComplete = std::move(onComplete)
](std::stop_token stopToken) mutable {
sol::state lua;
lua.open_libraries(sol::lib::base,
sol::lib::os,
@@ -65,40 +124,96 @@ void LuaRunner::run(int id, const std::filesystem::path& scriptPath, int timeout
sol::lib::utf8,
sol::lib::io);
LuaHookContext ctx{
cancelFlag,
deadlineFromTimeout(timeoutSeconds)
};
ToolLaunchBridge::registerFunctions(lua);
LuaHookContext ctx{cancelFlag, deadlineFromTimeout(timeoutSeconds)};
lua_State* L = lua.lua_state();
*reinterpret_cast<LuaHookContext**>(lua_getextraspace(L)) = &ctx;
*static_cast<LuaHookContext**>(lua_getextraspace(L)) = &ctx;
lua_sethook(L, luaCancelHook, LUA_MASKCOUNT, hookInstructionCount);
bool succeeded = false;
try {
std::cout << "Running Lua script: " << scriptPath.string() << std::endl;
lua.script_file(scriptPath.string());
} catch (const sol::error& e) {
if (stopToken.stop_requested()) {
cancelFlag->store(true, std::memory_order_relaxed);
}
std::cout << "Running Lua script: " << pathUtf8(scriptPath) << std::endl;
const sol::protected_function_result result =
lua.safe_script(script, sol::script_pass_on_error);
if (!result.valid()) {
const sol::error error = result;
throw error;
}
succeeded = true;
} catch (const std::exception& e) {
std::cerr << "Lua error: " << e.what() << std::endl;
}
{
std::lock_guard<std::mutex> lock(state->tasksMutex);
state->tasks.erase(id);
}
executionFinishedFlag->store(true, std::memory_order_release);
if (onComplete) {
onComplete();
onComplete(succeeded);
}
}).detach();
callbackFinishedFlag->store(true, std::memory_order_release);
});
}
std::cout << "Lua-Skript wurde gestartet." << std::endl;
return {true, singleInstance};
}
void LuaRunner::cancel(int id) {
LuaRunner::TaskStatus LuaRunner::status(const std::string& taskId,
const std::filesystem::path& scriptPath) const {
const bool scriptRequestsSingleInstance = requestsSingleInstance(readScript(scriptPath));
std::lock_guard<std::mutex> lock(m_state->tasksMutex);
const auto existing = m_state->tasks.find(taskId);
if (existing == m_state->tasks.end()) {
return {scriptRequestsSingleInstance, false};
}
bool running = false;
bool activeSingleInstance = false;
for (const auto& task : existing->second) {
if (!task.executionFinished->load(std::memory_order_acquire)) {
running = true;
activeSingleInstance = activeSingleInstance || task.singleInstance;
}
}
return {scriptRequestsSingleInstance || activeSingleInstance, running};
}
void LuaRunner::cancel(const std::string& taskId) const
{
auto state = m_state;
std::lock_guard<std::mutex> lock(state->tasksMutex);
auto it = state->tasks.find(id);
if (it != state->tasks.end() && it->second.cancel) {
it->second.cancel->store(true, std::memory_order_relaxed);
auto it = state->tasks.find(taskId);
if (it != state->tasks.end()) {
for (auto& task : it->second) {
task.cancel->store(true, std::memory_order_relaxed);
task.worker.request_stop();
}
}
}
void LuaRunner::shutdown() const
{
std::unordered_map<std::string, std::vector<LuaTask>> tasks;
{
std::lock_guard<std::mutex> lock(m_state->tasksMutex);
tasks.swap(m_state->tasks);
}
for (auto& instances : tasks | std::views::values) {
for (auto& task : instances) {
task.cancel->store(true, std::memory_order_relaxed);
task.worker.request_stop();
}
}
for (auto& instances : tasks | std::views::values) {
for (auto& task : instances) {
if (task.worker.joinable()) {
task.worker.join();
}
}
}
}

View File

@@ -1,12 +1,98 @@
#include "optionWindow.h"
#include "toolTemplates.h"
#include <filesystem>
#include <iostream>
#include <windows.h>
#include <shobjidl.h>
#include <wrl/client.h>
#include <nlohmann/json.hpp>
#include <saucer/embedded/all.hpp>
#include <saucer/navigation.hpp>
#include <saucer/app.hpp>
#include <unordered_map>
namespace {
using Microsoft::WRL::ComPtr;
struct PathSelection {
std::filesystem::path path;
bool cancelled = false;
std::string error;
};
PathSelection selectPath(HWND owner, bool selectFolder) {
ComPtr<IFileOpenDialog> dialog;
if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&dialog)))) {
return {{}, false, "Der Windows-Auswahldialog konnte nicht geöffnet werden."};
}
FILEOPENDIALOGOPTIONS options{};
dialog->GetOptions(&options);
options |= FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST;
if (selectFolder) {
options |= FOS_PICKFOLDERS;
dialog->SetTitle(L"Ordner für das Toolbox-Tool auswählen");
} else {
options |= FOS_FILEMUSTEXIST;
dialog->SetTitle(L"Programm für das Toolbox-Tool auswählen");
const COMDLG_FILTERSPEC filters[] = {
{L"Windows-Programme", L"*.exe"},
{L"Alle Dateien", L"*.*"},
};
dialog->SetFileTypes(static_cast<UINT>(std::size(filters)), filters);
dialog->SetFileTypeIndex(1);
}
dialog->SetOptions(options);
const HRESULT shown = dialog->Show(owner);
if (shown == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
return {{}, true, {}};
}
if (FAILED(shown)) {
return {{}, false, "Die Auswahl konnte nicht geöffnet werden."};
}
ComPtr<IShellItem> item;
PWSTR rawPath = nullptr;
if (FAILED(dialog->GetResult(&item)) ||
FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &rawPath))) {
return {{}, false, "Der ausgewählte Pfad konnte nicht gelesen werden."};
}
const std::filesystem::path selected(rawPath);
CoTaskMemFree(rawPath);
return {selected, false, {}};
}
std::string templateJson(const ToolTemplateResult& result) {
nlohmann::json response = {
{"success", result.success},
{"message", result.message},
};
if (result.success) {
response["draft"] = {
{"name", result.draft.name},
{"description", result.draft.description},
{"luaScript", result.draft.luaScript},
{"templateType", result.draft.templateType},
{"templateTarget", result.draft.templateTarget},
};
}
return response.dump();
}
std::string selectionJson(const PathSelection& selection,
ToolTemplateResult (*createTemplate)(const std::filesystem::path&)) {
if (selection.cancelled) {
return nlohmann::json({{"success", false}, {"cancelled", true}}).dump();
}
if (!selection.error.empty()) {
return nlohmann::json({{"success", false}, {"message", selection.error}}).dump();
}
return templateJson(createTemplate(selection.path));
}
std::unordered_map<HWND, WNDPROC> g_option_prev_procs;
LRESULT CALLBACK option_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
@@ -39,11 +125,11 @@ namespace {
}
optionWindow::optionWindow(std::shared_ptr<saucer::application>& app,
std::function<int()> getLuaTimeoutSeconds,
std::function<void(int)> setLuaTimeoutSeconds,
std::function<std::string()> getActiveToolName,
std::function<std::string()> getToolConfigIni,
std::function<void(const std::string &)> setToolConfigIni,
std::function<std::string()> getToolDetails,
std::function<std::string(const std::string &)> saveToolDetails,
std::function<std::string()> openToolFolder,
std::function<std::string(const std::string &)> createTool,
std::function<void()> openLuaHelp,
std::function<void()> openIndex)
: m_app(app)
, m_webview([&]() -> saucer::smartview {
@@ -52,6 +138,7 @@ optionWindow::optionWindow(std::shared_ptr<saucer::application>& app,
throw std::runtime_error("optionWindow: failed to create saucer::window");
}
saucer::webview::options opts{window_res.value()};
opts.storage_path = std::filesystem::temp_directory_path() / "toolbox-option-webview2";
opts.browser_flags.insert("--disable-gpu");
auto sv_res = saucer::smartview::create(opts);
if (!sv_res.has_value()) {
@@ -60,11 +147,11 @@ optionWindow::optionWindow(std::shared_ptr<saucer::application>& app,
return std::move(sv_res.value());
}())
, m_hwnd()
, m_getLuaTimeoutSeconds(std::move(getLuaTimeoutSeconds))
, m_setLuaTimeoutSeconds(std::move(setLuaTimeoutSeconds))
, m_getActiveToolName(std::move(getActiveToolName))
, m_getToolConfigIni(std::move(getToolConfigIni))
, m_setToolConfigIni(std::move(setToolConfigIni))
, m_getToolDetails(std::move(getToolDetails))
, m_saveToolDetails(std::move(saveToolDetails))
, m_openToolFolder(std::move(openToolFolder))
, m_createTool(std::move(createTool))
, m_openLuaHelp(std::move(openLuaHelp))
, m_openIndex(std::move(openIndex))
{
}
@@ -110,6 +197,12 @@ void optionWindow::configureWindow() {
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
RECT workArea{};
if (SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0)) {
screenWidth = static_cast<int>(workArea.right - workArea.left);
screenHeight = static_cast<int>(workArea.bottom - workArea.top);
}
int windowWidth = static_cast<int>(screenWidth * (500.0 / 1920.0));
int windowHeight = static_cast<int>(screenHeight * (700.0 / 1080.0));
@@ -119,10 +212,9 @@ void optionWindow::configureWindow() {
m_webview.parent().set_background(bg);
m_webview.parent().set_decorations(saucer::window::decoration::none);
int xPos = (screenWidth - windowWidth) / 2;
int yPos = (screenHeight - windowHeight) / 2;
int xPos = workArea.left + (screenWidth - windowWidth) / 2;
int yPos = workArea.top + (screenHeight - windowHeight) / 2;
// HWND über das Modul abrufen
if (HWND hwnd = m_hwnd) {
SetWindowPos(hwnd, nullptr, xPos, yPos, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
@@ -164,9 +256,17 @@ void optionWindow::runLuaScript() {
}
void optionWindow::show() {
m_webview.parent().show();
runLuaScript();
m_webview.serve("/html/einstellungen_custom.html");
configureWindow();
m_webview.parent().show();
m_webview.parent().focus();
}
void optionWindow::showCreateTool() {
m_webview.serve("/html/add_tool.html");
configureWindow();
m_webview.parent().show();
m_webview.parent().focus();
}
void optionWindow::exposeToJs() {
@@ -177,36 +277,43 @@ void optionWindow::exposeToJs() {
m_webview.parent().hide();
});
m_webview.expose("getLuaTimeoutSeconds", [&]() -> int {
if (m_getLuaTimeoutSeconds) {
return m_getLuaTimeoutSeconds();
}
return 30;
m_webview.expose("getToolDetails", [&]() -> std::string {
return m_getToolDetails ? m_getToolDetails() :
R"({"success":false,"message":"Kein Tool ist ausgewaehlt."})";
});
m_webview.expose("setLuaTimeoutSeconds", [&](int seconds) {
if (m_setLuaTimeoutSeconds) {
m_setLuaTimeoutSeconds(seconds);
}
m_webview.expose("saveToolDetails", [&](const std::string& payload) -> std::string {
return m_saveToolDetails ? m_saveToolDetails(payload) :
R"({"success":false,"message":"Das Tool kann nicht gespeichert werden."})";
});
m_webview.expose("getActiveToolName", [&]() -> std::string {
if (m_getActiveToolName) {
return m_getActiveToolName();
}
return {};
m_webview.expose("openToolFolder", [&]() -> std::string {
return m_openToolFolder ? m_openToolFolder() :
R"({"success":false,"message":"Der Toolordner kann nicht geoeffnet werden."})";
});
m_webview.expose("getToolConfigIni", [&]() -> std::string {
if (m_getToolConfigIni) {
return m_getToolConfigIni();
m_webview.expose("createTool", [&](const std::string &payload) -> std::string {
if (m_createTool) {
return m_createTool(payload);
}
return {};
return R"({"success":false,"message":"Tool-Erstellung ist nicht verfuegbar."})";
});
m_webview.expose("setToolConfigIni", [&](const std::string &content) {
if (m_setToolConfigIni) {
m_setToolConfigIni(content);
m_webview.expose("selectProgramTemplate", [&]() -> std::string {
return selectionJson(selectPath(m_hwnd, false), &ToolTemplates::programLauncher);
});
m_webview.expose("selectFolderTemplate", [&]() -> std::string {
return selectionJson(selectPath(m_hwnd, true), &ToolTemplates::folderLauncher);
});
m_webview.expose("createWebsiteTemplate", [&](const std::string& url) -> std::string {
return templateJson(ToolTemplates::websiteLauncher(url));
});
m_webview.expose("openLuaHelp", [&]() {
if (m_openLuaHelp) {
m_openLuaHelp();
}
});
}

60
src/toolLaunchBridge.cpp Normal file
View File

@@ -0,0 +1,60 @@
#include "toolLaunchBridge.h"
#include <sol/sol.hpp>
#include <cstdint>
#include <string>
#include <tuple>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#endif
namespace {
#ifdef _WIN32
std::wstring utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
}
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), nullptr, 0);
if (length <= 0) {
return {};
}
std::wstring result(static_cast<std::size_t>(length), L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
static_cast<int>(value.size()), result.data(), length) <= 0) {
return {};
}
return result;
}
std::tuple<bool, std::string> openNativeTarget(const std::string& target) {
const std::wstring wideTarget = utf8ToWide(target);
if (wideTarget.empty()) {
return {false, "Das Ziel ist leer oder nicht gueltiges UTF-8."};
}
const auto result = reinterpret_cast<std::intptr_t>(
ShellExecuteW(nullptr, L"open", wideTarget.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result <= 32) {
return {false, "Windows konnte das Ziel nicht oeffnen (Fehler " +
std::to_string(static_cast<long long>(result)) + ")."};
}
return {true, {}};
}
#else
std::tuple<bool, std::string> openNativeTarget(const std::string&) {
return {false, "Native Vorlagenstarts werden auf diesem System nicht unterstuetzt."};
}
#endif
} // namespace
void ToolLaunchBridge::registerFunctions(sol::state& lua) {
auto toolbox = lua.create_named_table("toolbox");
toolbox.set_function("open_program", &openNativeTarget);
toolbox.set_function("open_folder", &openNativeTarget);
toolbox.set_function("open_url", &openNativeTarget);
}

View File

@@ -1,9 +1,303 @@
#include "toolRegistry.h"
#include "toolTemplates.h"
#include <cppcodec/base64_rfc4648.hpp>
#include <nlohmann/json.hpp>
#include <chrono>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <ranges>
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <unordered_set>
#ifdef _WIN32
#include <windows.h>
#endif
namespace {
std::string trim(std::string value) {
const auto isNotSpace = [](unsigned char character) { return !std::isspace(character); };
value.erase(value.begin(), std::find_if(value.begin(), value.end(), isNotSpace));
value.erase(std::find_if(value.rbegin(), value.rend(), isNotSpace).base(), value.end());
return value;
}
bool isValidToolName(const std::string& name) {
if (name.empty() || name.size() > 80 || name == "." || name == ".." ||
name.back() == '.' || name.back() == ' ') {
return false;
}
constexpr std::string_view invalidCharacters = "<>:\"/\\|?*";
if (std::ranges::any_of(name, [](unsigned char character) {
return character < 32;
}) || name.find_first_of(invalidCharacters) != std::string::npos) {
return false;
}
std::string deviceName = name.substr(0, name.find('.'));
std::ranges::transform(deviceName, deviceName.begin(), [](unsigned char character) {
return static_cast<char>(std::toupper(character));
});
static const std::unordered_set<std::string> reservedNames = {
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
};
return !reservedNames.contains(deviceName);
}
bool isTemplateType(const std::string& type) {
return type == "lua" || type == "program" || type == "folder" || type == "website";
}
bool writeTextFile(const std::filesystem::path& path, const std::string& content) {
std::ofstream output(path, std::ios::binary);
output.write(content.data(), static_cast<std::streamsize>(content.size()));
return output.good();
}
std::string readTextFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
return {};
}
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
std::string pathUtf8(const std::filesystem::path& path) {
const auto value = path.u8string();
return {reinterpret_cast<const char*>(value.data()), value.size()};
}
bool isInternalEntryName(const std::string& name) {
return name.starts_with(".toolbox-update-") ||
name.starts_with(".toolbox-backup-") ||
name.starts_with(".toolbox-transaction-");
}
bool isReparsePoint(const std::filesystem::path& path) {
#ifdef _WIN32
const DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
#else
std::error_code error;
return std::filesystem::is_symlink(std::filesystem::symlink_status(path, error));
#endif
}
bool isDirectToolPath(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath) {
const auto toolboxRoot = userPath / ".Toolbox";
if (isReparsePoint(toolboxRoot) || isReparsePoint(toolPath)) {
return false;
}
std::error_code error;
const auto root = std::filesystem::weakly_canonical(toolboxRoot, error);
if (error) {
return false;
}
const auto parent = std::filesystem::weakly_canonical(toolPath.parent_path(), error);
if (error || parent != root || isInternalEntryName(pathUtf8(toolPath.filename()))) {
return false;
}
constexpr std::array<std::string_view, 5> managedFiles = {
"description.txt", "start.lua", "config.ini", "tool.json", "icon.png"
};
return std::ranges::none_of(managedFiles, [&toolPath](std::string_view name) {
return isReparsePoint(toolPath / name);
});
}
bool isPng(const std::vector<unsigned char>& bytes) {
constexpr std::array<unsigned char, 8> signature = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
return bytes.size() >= signature.size() &&
std::equal(signature.begin(), signature.end(), bytes.begin());
}
void writeIcon(const std::filesystem::path& path, const std::string& base64) {
const auto bytes = cppcodec::base64_rfc4648::decode<std::vector<unsigned char>>(base64);
if (!isPng(bytes)) {
throw std::runtime_error("Das ausgewaehlte Icon ist keine gueltige PNG-Datei.");
}
std::ofstream output(path, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
if (!output.good()) {
throw std::runtime_error("Das Icon konnte nicht geschrieben werden.");
}
}
struct ResolvedScript {
bool success = false;
std::string message;
std::string script;
std::string type;
std::string target;
};
ResolvedScript resolveScript(std::string type, const std::string& target,
const std::string& luaScript, const std::string& name) {
type = trim(std::move(type));
if (type.empty() || type == "lua") {
return {
true,
{},
luaScript.empty() ? ToolTemplates::defaultLuaScript("Hallo aus " + name) : luaScript,
"lua",
{},
};
}
ToolTemplateResult generated;
if (type == "program") {
generated = ToolTemplates::programLauncher(std::filesystem::u8path(target));
} else if (type == "folder") {
generated = ToolTemplates::folderLauncher(std::filesystem::u8path(target));
} else if (type == "website") {
generated = ToolTemplates::websiteLauncher(target);
} else {
return {false, "Der Starttyp ist ungueltig."};
}
if (!generated.success) {
return {false, generated.message};
}
return {true, {}, generated.draft.luaScript, type, generated.draft.templateTarget};
}
std::string metadataJson(const std::string& type, const std::string& target) {
return nlohmann::json{{"version", 1}, {"type", type}, {"target", target}}.dump(2) + "\n";
}
int readTimeout(const std::filesystem::path& path) {
std::istringstream input(readTextFile(path));
std::string line;
while (std::getline(input, line)) {
constexpr std::string_view prefix = "lua_timeout_seconds=";
if (!line.starts_with(prefix)) {
continue;
}
try {
return std::clamp(std::stoi(line.substr(prefix.size())), 0, 86400);
} catch (...) {
return 30;
}
}
return 30;
}
std::string configWithTimeout(const std::filesystem::path& path, int seconds) {
std::istringstream input(readTextFile(path));
std::vector<std::string> lines;
std::string line;
bool replaced = false;
while (std::getline(input, line)) {
if (line.starts_with("lua_timeout_seconds=")) {
line = "lua_timeout_seconds=" + std::to_string(std::clamp(seconds, 0, 86400));
replaced = true;
}
lines.push_back(std::move(line));
}
if (!replaced) {
lines.push_back("lua_timeout_seconds=" + std::to_string(std::clamp(seconds, 0, 86400)));
}
std::string result;
for (const auto& configLine : lines) {
result += configLine + "\n";
}
return result;
}
std::string readIconBase64(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary | std::ios::ate);
if (!input) {
return {};
}
const auto size = input.tellg();
input.seekg(0, std::ios::beg);
std::vector<unsigned char> bytes(static_cast<std::size_t>(size));
if (!input.read(reinterpret_cast<char*>(bytes.data()), size)) {
return {};
}
return cppcodec::base64_rfc4648::encode(bytes);
}
bool recoverTransaction(const std::filesystem::path& root,
const std::filesystem::path& journalPath) {
try {
const auto journal = nlohmann::json::parse(readTextFile(journalPath));
const std::string id = journal.at("id").get<std::string>();
const std::string originalName = journal.at("originalName").get<std::string>();
const std::string targetName = journal.at("targetName").get<std::string>();
const std::string expectedJournalName = ".toolbox-transaction-" + id + ".json";
if (id.empty() ||
std::ranges::any_of(id, [](unsigned char character) { return !std::isdigit(character); }) ||
pathUtf8(journalPath.filename()) != expectedJournalName ||
!isValidToolName(originalName) || !isValidToolName(targetName)) {
return false;
}
const auto stagePath = root / (".toolbox-update-" + id);
const auto backupPath = root / (".toolbox-backup-" + id);
const auto originalPath = root / std::filesystem::u8path(originalName);
const auto targetPath = root / std::filesystem::u8path(targetName);
std::error_code error;
if (std::filesystem::exists(backupPath)) {
if (std::filesystem::exists(targetPath) && !std::filesystem::exists(stagePath)) {
std::filesystem::remove_all(backupPath, error);
} else if (!std::filesystem::exists(originalPath)) {
std::filesystem::rename(backupPath, originalPath, error);
} else {
std::filesystem::remove_all(backupPath, error);
}
if (error) {
return false;
}
}
std::filesystem::remove_all(stagePath, error);
if (error) {
return false;
}
std::filesystem::remove(journalPath, error);
return !error;
} catch (...) {
return false;
}
}
void recoverTransactions(const std::filesystem::path& root) {
std::vector<std::filesystem::path> journals;
for (const auto& entry : std::filesystem::directory_iterator(root)) {
const std::string name = pathUtf8(entry.path().filename());
if (entry.is_regular_file() && name.starts_with(".toolbox-transaction-") &&
entry.path().extension() == ".json") {
journals.push_back(entry.path());
}
}
for (const auto& journal : journals) {
recoverTransaction(root, journal);
}
for (const auto& entry : std::filesystem::directory_iterator(root)) {
const std::string name = pathUtf8(entry.path().filename());
if (entry.is_directory() && name.starts_with(".toolbox-update-")) {
std::error_code ignored;
std::filesystem::remove_all(entry.path(), ignored);
}
}
}
} // namespace
std::filesystem::path ToolRegistry::toolboxDirectory(const std::filesystem::path& userPath) {
return userPath / ".Toolbox";
@@ -12,12 +306,11 @@ std::filesystem::path ToolRegistry::toolboxDirectory(const std::filesystem::path
bool ToolRegistry::ensureToolboxDirectory(const std::filesystem::path& userPath) {
const std::filesystem::path toolboxPath = toolboxDirectory(userPath);
if (std::filesystem::exists(toolboxPath)) {
return true;
return std::filesystem::is_directory(toolboxPath) && !isReparsePoint(toolboxPath);
}
try {
if (std::filesystem::create_directory(toolboxPath)) {
std::cout << ".Toolbox Verzeichnis wurde erstellt." << std::endl;
return true;
}
@@ -33,21 +326,31 @@ std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& us
std::vector<ToolboxItem> items;
const std::filesystem::path toolboxDir = toolboxDirectory(userPath);
if (!std::filesystem::exists(toolboxDir) || !std::filesystem::is_directory(toolboxDir)) {
if (!std::filesystem::exists(toolboxDir) || !std::filesystem::is_directory(toolboxDir) ||
isReparsePoint(toolboxDir)) {
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
return items;
}
recoverTransactions(toolboxDir);
int id = 0;
for (const auto& entry : std::filesystem::directory_iterator(toolboxDir)) {
if (!entry.is_directory()) {
continue;
}
const std::string entryName = pathUtf8(entry.path().filename());
if (isInternalEntryName(entryName)) {
continue;
}
if (!isDirectToolPath(userPath, entry.path())) {
continue;
}
ToolboxItem item;
item.id = std::to_string(id++);
item.name = entry.path().filename().string();
item.path = entry.path().string();
item.name = entryName;
item.path = pathUtf8(entry.path());
encodeIcon(entry.path() / "icon.png", item);
@@ -59,7 +362,7 @@ std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& us
const std::filesystem::path luaScript = entry.path() / "start.lua";
if (std::filesystem::exists(luaScript)) {
item.lua_script_path_string = luaScript.string();
item.lua_script_path_string = pathUtf8(luaScript);
}
items.push_back(std::move(item));
@@ -68,7 +371,183 @@ std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& us
return items;
}
ToolCreationResult ToolRegistry::createTool(const std::filesystem::path& userPath, const ToolDraft& draft) {
const std::string name = trim(draft.name);
if (!isValidToolName(name)) {
return {false, "Der Tool-Name ist leer oder enthaelt ungueltige Zeichen."};
}
if (draft.description.size() > 8 * 1024 || draft.luaScript.size() > 1024 * 1024 ||
draft.iconBase64.size() > 3 * 1024 * 1024) {
return {false, "Mindestens eine Eingabe ist zu gross."};
}
if (!ensureToolboxDirectory(userPath)) {
return {false, "Das .Toolbox-Verzeichnis konnte nicht erstellt werden."};
}
const std::filesystem::path toolPath = toolboxDirectory(userPath) / std::filesystem::u8path(name);
const auto resolved = resolveScript(draft.templateType, draft.templateTarget, draft.luaScript, name);
if (!resolved.success) {
return {false, resolved.message};
}
try {
if (!std::filesystem::create_directory(toolPath)) {
return {false, "Ein Tool mit diesem Namen existiert bereits."};
}
if (!writeTextFile(toolPath / "description.txt", draft.description) ||
!writeTextFile(toolPath / "start.lua", resolved.script) ||
!writeTextFile(toolPath / "config.ini",
"lua_timeout_seconds=" + std::to_string(std::clamp(draft.timeoutSeconds, 0, 86400)) + "\n") ||
!writeTextFile(toolPath / "tool.json", metadataJson(resolved.type, resolved.target))) {
throw std::runtime_error("Tool-Dateien konnten nicht geschrieben werden.");
}
if (!draft.iconBase64.empty()) {
writeIcon(toolPath / "icon.png", draft.iconBase64);
}
} catch (const std::exception& error) {
std::error_code cleanupError;
std::filesystem::remove_all(toolPath, cleanupError);
return {false, error.what()};
}
return {true, "Tool wurde erstellt."};
}
ToolDetailsResult ToolRegistry::getToolDetails(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath) {
if (!isDirectToolPath(userPath, toolPath) || !std::filesystem::is_directory(toolPath)) {
return {false, "Das Tool-Verzeichnis wurde nicht gefunden."};
}
ToolDetails details{
.path = toolPath,
.name = pathUtf8(toolPath.filename()),
.description = readTextFile(toolPath / "description.txt"),
.luaScript = readTextFile(toolPath / "start.lua"),
.iconBase64 = readIconBase64(toolPath / "icon.png"),
.timeoutSeconds = readTimeout(toolPath / "config.ini"),
};
const auto metadataPath = toolPath / "tool.json";
if (std::filesystem::exists(metadataPath)) {
try {
const auto metadata = nlohmann::json::parse(readTextFile(metadataPath));
details.templateType = metadata.value("type", "lua");
details.templateTarget = metadata.value("target", "");
if (!isTemplateType(details.templateType)) {
details.templateType = "lua";
details.templateTarget.clear();
}
} catch (...) {
details.templateType = "lua";
details.templateTarget.clear();
}
}
return {true, {}, std::move(details)};
}
ToolUpdateResult ToolRegistry::updateTool(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath,
const ToolUpdateDraft& draft) {
const std::string name = trim(draft.name);
if (!isDirectToolPath(userPath, toolPath) || !std::filesystem::is_directory(toolPath)) {
return {false, "Das Tool-Verzeichnis wurde nicht gefunden."};
}
if (!isValidToolName(name)) {
return {false, "Der Tool-Name ist leer oder enthaelt ungueltige Zeichen."};
}
if (draft.description.size() > 8 * 1024 || draft.luaScript.size() > 1024 * 1024 ||
(draft.iconBase64 && draft.iconBase64->size() > 3 * 1024 * 1024)) {
return {false, "Mindestens eine Eingabe ist zu gross."};
}
const auto resolved = resolveScript(draft.templateType, draft.templateTarget,
draft.luaScript, name);
if (!resolved.success) {
return {false, resolved.message};
}
const auto parent = toolPath.parent_path();
const auto targetPath = parent / std::filesystem::u8path(name);
if (targetPath != toolPath && std::filesystem::exists(targetPath)) {
return {false, "Ein Tool mit diesem Namen existiert bereits."};
}
const auto suffix = std::to_string(static_cast<std::uint64_t>(
std::chrono::steady_clock::now().time_since_epoch().count()));
const auto stagePath = parent / (".toolbox-update-" + suffix);
const auto backupPath = parent / (".toolbox-backup-" + suffix);
const auto journalPath = parent / (".toolbox-transaction-" + suffix + ".json");
try {
std::filesystem::copy(toolPath, stagePath, std::filesystem::copy_options::recursive);
if (!writeTextFile(stagePath / "description.txt", draft.description) ||
!writeTextFile(stagePath / "start.lua", resolved.script) ||
!writeTextFile(stagePath / "config.ini",
configWithTimeout(stagePath / "config.ini", draft.timeoutSeconds)) ||
!writeTextFile(stagePath / "tool.json", metadataJson(resolved.type, resolved.target))) {
throw std::runtime_error("Tool-Dateien konnten nicht geschrieben werden.");
}
if (draft.iconBase64) {
if (draft.iconBase64->empty()) {
std::filesystem::remove(stagePath / "icon.png");
} else {
writeIcon(stagePath / "icon.png", *draft.iconBase64);
}
}
const std::string journal = nlohmann::json{
{"version", 1},
{"id", suffix},
{"originalName", pathUtf8(toolPath.filename())},
{"targetName", name},
}.dump(2) + "\n";
if (!writeTextFile(journalPath, journal)) {
throw std::runtime_error("Das Update-Journal konnte nicht geschrieben werden.");
}
std::filesystem::rename(toolPath, backupPath);
std::filesystem::rename(stagePath, targetPath);
std::error_code cleanupError;
std::filesystem::remove_all(backupPath, cleanupError);
if (!cleanupError) {
std::filesystem::remove(journalPath, cleanupError);
}
} catch (const std::exception& error) {
if (!recoverTransaction(parent, journalPath)) {
std::error_code ignored;
std::filesystem::remove_all(stagePath, ignored);
if (std::filesystem::exists(backupPath) && !std::filesystem::exists(toolPath)) {
std::filesystem::rename(backupPath, toolPath, ignored);
}
}
return {false, error.what()};
}
return {true, "Tool wurde gespeichert.", targetPath};
}
ToolDeletionResult ToolRegistry::deleteTool(const std::filesystem::path& userPath,
const std::filesystem::path& toolPath) {
if (!isDirectToolPath(userPath, toolPath) || !std::filesystem::is_directory(toolPath)) {
return {false, "Das Tool-Verzeichnis wurde nicht gefunden."};
}
std::error_code error;
std::filesystem::remove_all(toolPath, error);
if (error) {
return {false, "Das Tool konnte nicht geloescht werden."};
}
return {true, "Tool wurde geloescht."};
}
void ToolRegistry::encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item) {
if (!std::filesystem::exists(imagePath)) {
return;
}
std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Fehler beim Oeffnen des PNG-Bildes: " << imagePath << std::endl;

130
src/toolTemplates.cpp Normal file
View File

@@ -0,0 +1,130 @@
#include "toolTemplates.h"
#include <algorithm>
#include <cctype>
#include <string_view>
namespace {
std::string luaStringLiteral(std::string_view value) {
std::string result;
result.reserve(value.size() + 2);
result.push_back('"');
for (const char character : value) {
switch (character) {
case '\\': result += "\\\\"; break;
case '"': result += "\\\""; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default: result.push_back(character); break;
}
}
result.push_back('"');
return result;
}
bool validName(const std::string& name) {
return !name.empty() && name.size() <= 80;
}
std::string pathUtf8(const std::filesystem::path& path) {
const auto encoded = path.u8string();
return {reinterpret_cast<const char*>(encoded.data()), encoded.size()};
}
std::string launcherScript(std::string_view operation,
std::string_view functionName,
const std::string& value) {
return "local target = " + luaStringLiteral(value) + "\n"
"local ok, message = toolbox." + std::string(functionName) + "(target)\n"
"if not ok then\n"
" error('" + std::string(operation) + " fehlgeschlagen: ' .. tostring(message))\n"
"end\n";
}
std::string lower(std::string value) {
std::ranges::transform(value, value.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
} // namespace
std::string ToolTemplates::defaultLuaScript(std::string_view toolName) {
return "print(" + luaStringLiteral(toolName) + ")\n";
}
ToolTemplateResult ToolTemplates::programLauncher(const std::filesystem::path& executable) {
const std::string name = pathUtf8(executable.stem());
if (!validName(name) || lower(pathUtf8(executable.extension())) != ".exe") {
return {false, "Bitte eine gültige Windows-Programmdatei (.exe) auswählen.", {}};
}
return {
true,
{},
{
name,
"Startet " + name + ".",
launcherScript("Programmstart", "open_program", pathUtf8(executable)),
"program",
pathUtf8(executable),
}
};
}
ToolTemplateResult ToolTemplates::folderLauncher(const std::filesystem::path& folder) {
const std::string name = pathUtf8(folder.filename());
if (!validName(name)) {
return {false, "Bitte einen gültigen Ordner auswählen.", {}};
}
return {
true,
{},
{
name + " öffnen",
"Öffnet den Ordner " + name + ".",
launcherScript("Öffnen des Ordners", "open_folder", pathUtf8(folder)),
"folder",
pathUtf8(folder),
}
};
}
ToolTemplateResult ToolTemplates::websiteLauncher(const std::string& url) {
const std::size_t schemeLength = url.starts_with("https://") ? 8
: url.starts_with("http://") ? 7 : 0;
if (schemeLength == 0 || url.size() > 2048 ||
std::ranges::any_of(url, [](unsigned char character) {
return character <= 32 || character == '\\' || character == '"';
})) {
return {false, "Bitte eine gültige HTTP- oder HTTPS-URL eingeben.", {}};
}
const std::size_t hostEnd = url.find_first_of("/?#", schemeLength);
std::string host = url.substr(schemeLength, hostEnd - schemeLength);
if (host.find('@') != std::string::npos) {
return {false, "URLs mit eingebetteten Anmeldedaten werden nicht unterstützt.", {}};
}
if (host.starts_with("www.")) {
host.erase(0, 4);
}
if (!validName(host)) {
return {false, "Die URL enthält keinen gültigen Hostnamen.", {}};
}
return {
true,
{},
{
host + " öffnen",
"Öffnet " + url + ".",
launcherScript("Öffnen der Website", "open_url", url),
"website",
url,
}
};
}

View File

@@ -5,12 +5,12 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
return bindings.getToolboxItems();
});
webview.expose("run_lua", [bindings](int id) {
bindings.runLua(id);
webview.expose("run_lua", [bindings](const std::string& toolPath) -> std::string {
return bindings.runLua(toolPath);
});
webview.expose("cancel_lua", [bindings](int id) {
bindings.cancelLua(id);
webview.expose("cancel_lua", [bindings](const std::string& toolPath) {
bindings.cancelLua(toolPath);
});
webview.expose("debug_print", [bindings]() {
@@ -25,8 +25,24 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
bindings.openCommandPrompt();
});
webview.expose("openEinst", [bindings](int id) {
bindings.openSettings(id);
webview.expose("openAddTool", [bindings]() {
bindings.openAddTool();
});
webview.expose("openEinst", [bindings](const std::string& toolPath) {
bindings.openSettings(toolPath);
});
webview.expose("deleteTool", [bindings](const std::string& toolPath) -> std::string {
return bindings.deleteTool(toolPath);
});
webview.expose("setOpacity", [bindings](int percent) {
bindings.setOpacity(percent);
});
webview.expose("getOpacity", [bindings]() -> int {
return bindings.getOpacity();
});
webview.expose("openIndex", [bindings]() {
@@ -36,24 +52,4 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
webview.expose("getNewContent", [bindings]() {
bindings.getNewContent();
});
webview.expose("getLuaTimeoutSeconds", [bindings]() -> int {
return bindings.getLuaTimeoutSeconds();
});
webview.expose("setLuaTimeoutSeconds", [bindings](int seconds) {
bindings.setLuaTimeoutSeconds(seconds);
});
webview.expose("getActiveToolName", [bindings]() -> std::string {
return bindings.getActiveToolName();
});
webview.expose("getToolConfigIni", [bindings]() -> std::string {
return bindings.getToolConfigIni();
});
webview.expose("setToolConfigIni", [bindings](const std::string& content) {
bindings.setToolConfigIni(content);
});
}

View File

@@ -1,3 +1,11 @@
#include "toolboxWindow.h"
#include <iostream>
#include <stdexcept>
@@ -12,6 +20,7 @@
#undef min
#endif
#include <algorithm>
#include <array>
#include <nlohmann/json.hpp>
#include <saucer/navigation.hpp>
#include <saucer/app.hpp>
@@ -53,6 +62,24 @@ namespace {
}
}
std::filesystem::path ToolboxWindow::userDirectory() {
#ifdef _WIN32
const DWORD required = GetEnvironmentVariableW(L"USERPROFILE", nullptr, 0);
if (required > 1) {
std::wstring directory(required, L'\0');
const DWORD written = GetEnvironmentVariableW(L"USERPROFILE", directory.data(), required);
if (written > 0 && written < required) {
directory.resize(written);
return std::filesystem::path(directory);
}
}
return std::filesystem::current_path();
#else
const char* directory = std::getenv("HOME");
return directory ? std::filesystem::path(directory) : std::filesystem::current_path();
#endif
}
ToolboxWindow::ToolboxWindow(std::shared_ptr<saucer::application>& app)
: m_webview([&]() -> saucer::smartview {
auto window_res = saucer::window::create(app.get());
@@ -129,9 +156,9 @@ void ToolboxWindow::loadResources() {
void ToolboxWindow::show() {
m_webview.parent().show();
m_bShow = true;
m_isVisible.store(true, std::memory_order_release);
m_lastShowTime = std::chrono::steady_clock::now();
m_seenFocusSinceShow = false;
m_seenFocusSinceShow.store(false, std::memory_order_release);
// In some cases the HWND/frame metrics are finalized only after showing.
// Re-acquire HWND and reposition now, plus schedule a short delayed reposition.
@@ -151,33 +178,51 @@ void ToolboxWindow::show() {
configureWindow();
// Deferred reposition to catch late layouting from the framework
std::thread([this]() {
scheduleDelayedReposition();
}
void ToolboxWindow::scheduleDelayedReposition() {
if (m_repositionThread.joinable()) {
m_repositionThread.request_stop();
m_repositionThread.join();
}
m_repositionThread = std::jthread([this](std::stop_token stopToken) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
configureWindow();
}).detach();
if (!stopToken.stop_requested()) {
configureWindow();
}
});
}
void ToolboxWindow::hide() {
m_webview.parent().hide();
m_bShow = false;
m_isVisible.store(false, std::memory_order_release);
}
bool ToolboxWindow::isVisible() const noexcept {
return m_isVisible.load(std::memory_order_acquire);
}
void ToolboxWindow::ExposeToJs() {
ToolboxJsBridge::registerBindings(m_webview, {
.getToolboxItems = [this]() { return jsonString; },
.runLua = [this](int id) { run_lua(id); },
.cancelLua = [this](int id) { cancel_lua(id); },
.getToolboxItems = [this]() {
SetJsonItems();
return jsonString;
},
.runLua = [this](const std::string& toolPath) { return run_lua_path(toolPath); },
.cancelLua = [this](const std::string& toolPath) { cancel_lua(toolPath); },
.debugPrint = [this]() { debug_print(); },
.openFileSystem = [this]() { open_file_system(); },
.openCommandPrompt = [this]() { open_cmd(); },
.openSettings = [this](int id) { openEinst(id); },
.openAddTool = [this]() { openAddTool(); },
.openSettings = [this](const std::string& toolPath) { openEinst(toolPath); },
.deleteTool = [this](const std::string& toolPath) { return deleteTool(toolPath); },
.setOpacity = [this](int percent) { setWindowOpacity(percent); },
.getOpacity = [this]() { return getWindowOpacity(); },
.openIndex = [this]() { openIndex(); },
.getNewContent = [this]() { getNewContent(); },
.getLuaTimeoutSeconds = [this]() { return getLuaTimeoutSeconds(); },
.setLuaTimeoutSeconds = [this](int seconds) { setLuaTimeoutSeconds(seconds); },
.getActiveToolName = [this]() { return getActiveToolName(); },
.getToolConfigIni = [this]() { return getToolConfigIni(); },
.setToolConfigIni = [this](const std::string& content) { setToolConfigIni(content); },
});
}
@@ -185,6 +230,13 @@ void ToolboxWindow::SetJsonItems() {// Konvertierung des Vektors in JSON
nlohmann::json jsonArray = nlohmann::json::array();
for (const auto& item : m_toolboxItems) {
LuaRunner::TaskStatus taskStatus;
try {
taskStatus = m_luaRunner.status(
item.path, std::filesystem::u8path(item.lua_script_path_string));
} catch (const std::exception&) {
taskStatus = {};
}
nlohmann::json jsonItem = {
{"id", item.id},
{"name", item.name},
@@ -195,7 +247,9 @@ void ToolboxWindow::SetJsonItems() {// Konvertierung des Vektors in JSON
{"version", item.version},
{"author", item.author},
{"url", item.url},
{"license", item.license}
{"license", item.license},
{"singleInstance", taskStatus.singleInstance},
{"running", taskStatus.singleInstance && taskStatus.running}
};
jsonArray.push_back(jsonItem);
}
@@ -213,26 +267,62 @@ void ToolboxWindow::fillToolboxMenu() {
m_toolboxItems = ToolRegistry::loadTools(userPath);
}
void ToolboxWindow::run_lua(int id) {
LuaRunner::RunResult ToolboxWindow::run_lua(int id) {
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
return;
return {};
}
const std::filesystem::path scriptPath = m_toolboxItems[id].lua_script_path_string;
m_luaRunner.run(id, scriptPath, getLuaTimeoutSecondsForId(id), [this, id]() {
m_webview.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
m_webview.reload();
const std::filesystem::path scriptPath = std::filesystem::u8path(m_toolboxItems[id].lua_script_path_string);
const std::string toolPath = m_toolboxItems[id].path;
auto app = m_app;
auto alive = m_alive;
return m_luaRunner.run(toolPath, scriptPath, getLuaTimeoutSecondsForId(id),
[this, app = std::move(app), alive = std::move(alive)](bool) {
app->post([this, alive] {
if (!alive->load(std::memory_order_acquire)) {
return;
}
m_webview.saucer::webview::execute("reloadContent()");
});
});
}
void ToolboxWindow::cancel_lua(int id) {
m_luaRunner.cancel(id);
std::string ToolboxWindow::run_lua_path(const std::string& toolPath) {
const auto item = std::ranges::find(m_toolboxItems, toolPath, &ToolboxItem::path);
if (item == m_toolboxItems.end() ||
!ToolRegistry::getToolDetails(userDirectory(), std::filesystem::u8path(toolPath)).success) {
return nlohmann::json({
{"started", false}, {"singleInstance", false}, {"message", "Tool nicht gefunden."}
}).dump();
}
try {
const auto result = run_lua(static_cast<int>(std::distance(m_toolboxItems.begin(), item)));
return nlohmann::json({
{"started", result.started},
{"singleInstance", result.singleInstance},
{"message", result.started ? "Tool gestartet." : "Tool laeuft bereits."}
}).dump();
} catch (const std::exception& error) {
return nlohmann::json({
{"started", false}, {"singleInstance", false}, {"message", error.what()}
}).dump();
}
}
void ToolboxWindow::cancel_lua(const std::string& toolPath) {
m_luaRunner.cancel(toolPath);
}
void ToolboxWindow::debug_print() {
std::cout << "ToolboxWindow::debug_print()" << std::endl;
}
ToolboxWindow::~ToolboxWindow() {
m_alive->store(false, std::memory_order_release);
if (m_repositionThread.joinable()) {
m_repositionThread.request_stop();
m_repositionThread.join();
}
m_luaRunner.shutdown();
m_runningMonitor = false;
if (m_threadMonitor.joinable()) {
m_threadMonitor.join();
@@ -242,11 +332,12 @@ ToolboxWindow::~ToolboxWindow() {
void ToolboxWindow::monitor_focus() {
while (m_runningMonitor) {
if (m_bShow && isIndex) {
if (m_isVisible.load(std::memory_order_acquire) &&
m_isIndex.load(std::memory_order_acquire)) {
const bool focused = m_webview.parent().focused();
if (focused) {
m_seenFocusSinceShow = true;
} else if (m_seenFocusSinceShow) {
m_seenFocusSinceShow.store(true, std::memory_order_release);
} else if (m_seenFocusSinceShow.load(std::memory_order_acquire)) {
this->hide();
}
}
@@ -257,24 +348,28 @@ void ToolboxWindow::monitor_focus() {
}
void ToolboxWindow::open_file_system() {
if (const std::filesystem::path toolboxDir = userPath / ".Toolbox"; std::filesystem::exists(toolboxDir) && std::filesystem::is_directory(toolboxDir)) {
std::string path = toolboxDir.string();
std::string command;
if (const std::filesystem::path toolboxDir = userPath / ".Toolbox";
std::filesystem::exists(toolboxDir) && ToolRegistry::ensureToolboxDirectory(userPath)) {
#ifdef _WIN32
command = "explorer \"" + path + "\"";
const auto result = reinterpret_cast<std::intptr_t>(
ShellExecuteW(nullptr, L"open", toolboxDir.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result <= 32) {
std::cerr << "Fehler beim Oeffnen des Verzeichnisses: " << result << std::endl;
}
#elif __APPLE__
command = "open \"" + path + "\"";
const std::string command = "open \"" + toolboxDir.string() + "\"";
if (const int result = std::system(command.c_str()); result != 0) {
std::cerr << "Fehler beim Öffnen des Verzeichnisses: " << result << std::endl;
}
#elif __linux__
command = "xdg-open \"" + path + "\"";
const std::string command = "xdg-open \"" + toolboxDir.string() + "\"";
if (const int result = std::system(command.c_str()); result != 0) {
std::cerr << "Fehler beim Öffnen des Verzeichnisses: " << result << std::endl;
}
#else
std::cerr << "Fehler: Plattform nicht unterstützt." << std::endl;
return;
#endif
if (const int result = std::system(command.c_str()); result != 0) {
std::cerr << "Fehler beim Öffnen des Verzeichnisses: " << result << std::endl;
}
} else {
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
}
@@ -301,34 +396,107 @@ void ToolboxWindow::open_cmd() {
#endif
}
void ToolboxWindow::setWindowOpacity(int percent) {
if (percent < 70) percent = 70;
if (percent > 100) percent = 100;
m_lastOpacity = percent;
const double opacity = percent / 100.0;
m_webview.saucer::webview::execute(
"document.getElementById('shell').style.opacity = " + std::to_string(opacity));
}
// Beispielhafte Nutzung:
void ToolboxWindow::openEinst() {
int ToolboxWindow::getWindowOpacity() const {
return m_lastOpacity;
}
optionWindow& ToolboxWindow::optionWindowInstance() {
if (!m_optionWindow) {
m_optionWindow = std::make_unique<optionWindow>(
m_app,
[this]() { return getLuaTimeoutSeconds(); },
[this](int seconds) { setLuaTimeoutSeconds(seconds); },
[this]() { return getActiveToolName(); },
[this]() { return getToolConfigIni(); },
[this](const std::string &content) { setToolConfigIni(content); },
[this]() { return getActiveToolDetails(); },
[this](const std::string& payload) { return saveActiveToolDetails(payload); },
[this]() { return openActiveToolFolder(); },
[this](const std::string &payload) { return createTool(payload); },
[this]() { openLuaHelp(); },
[this]() { openIndex(); }
);
m_optionWindow->initialize();
}
m_optionWindow->show();
return *m_optionWindow;
}
void ToolboxWindow::openEinst(int id) {
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
std::cerr << "openEinst: invalid tool id " << id << std::endl;
void ToolboxWindow::openEinst() {
optionWindowInstance().show();
}
void ToolboxWindow::openEinst(const std::string& toolPath) {
const auto candidate = std::filesystem::u8path(toolPath);
if (!ToolRegistry::getToolDetails(userDirectory(), candidate).success) {
std::cerr << "openEinst: invalid tool path" << std::endl;
return;
}
m_activeToolId = id;
ensureConfigExists(id);
m_activeToolPath = candidate;
openEinst();
}
void ToolboxWindow::openAddTool() {
optionWindowInstance().showCreateTool();
}
LuaHelpWindow& ToolboxWindow::luaHelpWindowInstance() {
if (!m_luaHelpWindow) {
m_luaHelpWindow = std::make_unique<LuaHelpWindow>(m_app);
m_luaHelpWindow->initialize();
}
return *m_luaHelpWindow;
}
void ToolboxWindow::openLuaHelp() {
luaHelpWindowInstance().show();
}
std::string ToolboxWindow::createTool(const std::string& payload) {
nlohmann::json response;
try {
const auto input = nlohmann::json::parse(payload);
ToolDraft draft{
.name = input.value("name", ""),
.description = input.value("description", ""),
.luaScript = input.value("luaScript", ""),
.iconBase64 = input.value("iconBase64", ""),
.timeoutSeconds = input.value("timeoutSeconds", 30),
.templateType = input.value("templateType", "lua"),
.templateTarget = input.value("templateTarget", ""),
};
const ToolCreationResult result = ToolRegistry::createTool(userPath, draft);
response = {{"success", result.success}, {"message", result.message}};
if (result.success) {
fillToolboxMenu();
SetJsonItems();
}
} catch (const std::exception&) {
response = {{"success", false}, {"message", "Die Tool-Daten sind ungueltig."}};
}
return response.dump();
}
std::string ToolboxWindow::deleteTool(const std::string& toolPath) {
nlohmann::json response;
try {
const auto path = std::filesystem::u8path(toolPath);
const ToolDeletionResult result = ToolRegistry::deleteTool(userPath, path);
response = {{"success", result.success}, {"message", result.message}};
if (result.success) {
fillToolboxMenu();
SetJsonItems();
}
} catch (const std::exception&) {
response = {{"success", false}, {"message", "Das Tool konnte nicht geloescht werden."}};
}
return response.dump();
}
void ToolboxWindow::centerWindow() {
WindowPlacement::center(m_hwnd);
}
@@ -344,7 +512,7 @@ void ToolboxWindow::resizeWindow(double widthPercent, double heightPercent)
}
void ToolboxWindow::openIndex() {
if (!m_bShow) {
if (!m_isVisible.load(std::memory_order_acquire)) {
show();
}
if (!serveHtmlFile("html/index.html", false)) {
@@ -359,7 +527,7 @@ void ToolboxWindow::getNewContent()
fillToolboxMenu();
SetJsonItems();
m_webview.saucer::webview::execute("reloadContent()");
isIndex = true;
m_isIndex.store(true, std::memory_order_release);
}
void ToolboxWindow::toggleDevTools() {
@@ -370,15 +538,7 @@ std::filesystem::path ToolboxWindow::getToolConfigPath(int id) const {
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
return {};
}
return std::filesystem::path(m_toolboxItems[id].path) / "config.ini";
}
void ToolboxWindow::ensureConfigExists(int id) const {
const auto configPath = getToolConfigPath(id);
if (configPath.empty()) {
return;
}
m_configStore.ensureExists(configPath);
return std::filesystem::u8path(m_toolboxItems[id].path) / "config.ini";
}
int ToolboxWindow::getLuaTimeoutSecondsForId(int id) const {
@@ -389,50 +549,76 @@ int ToolboxWindow::getLuaTimeoutSecondsForId(int id) const {
return m_configStore.getInt(configPath, "lua_timeout_seconds", 30);
}
int ToolboxWindow::getLuaTimeoutSeconds() {
if (m_activeToolId < 0) {
return 30;
std::string ToolboxWindow::getActiveToolDetails() const {
const auto result = ToolRegistry::getToolDetails(userDirectory(), m_activeToolPath);
nlohmann::json response{{"success", result.success}, {"message", result.message}};
if (result.success) {
response["details"] = {
{"name", result.details.name},
{"description", result.details.description},
{"luaScript", result.details.luaScript},
{"iconBase64", result.details.iconBase64},
{"timeoutSeconds", result.details.timeoutSeconds},
{"templateType", result.details.templateType},
{"templateTarget", result.details.templateTarget},
};
}
return getLuaTimeoutSecondsForId(m_activeToolId);
return response.dump();
}
void ToolboxWindow::setLuaTimeoutSeconds(int seconds) {
if (m_activeToolId < 0) {
return;
std::string ToolboxWindow::saveActiveToolDetails(const std::string& payload) {
nlohmann::json response;
try {
const auto input = nlohmann::json::parse(payload);
const std::array<std::string_view, 5> stringFields = {
"name", "description", "luaScript", "templateType", "templateTarget"
};
if (!input.is_object() ||
std::ranges::any_of(stringFields, [&input](std::string_view field) {
return !input.contains(field) || !input.at(field).is_string();
}) || !input.contains("timeoutSeconds") || !input.at("timeoutSeconds").is_number_integer() ||
(input.contains("iconBase64") && !input.at("iconBase64").is_string())) {
return nlohmann::json({
{"success", false},
{"message", "Die Tool-Daten haben ein ungueltiges Format."}
}).dump();
}
std::optional<std::string> iconBase64;
if (input.contains("iconBase64")) {
iconBase64 = input.value("iconBase64", "");
}
const ToolUpdateResult result = ToolRegistry::updateTool(userDirectory(), m_activeToolPath, ToolUpdateDraft{
.name = input.value("name", ""),
.description = input.value("description", ""),
.luaScript = input.value("luaScript", ""),
.iconBase64 = std::move(iconBase64),
.timeoutSeconds = input.value("timeoutSeconds", 30),
.templateType = input.value("templateType", "lua"),
.templateTarget = input.value("templateTarget", ""),
});
response = {{"success", result.success}, {"message", result.message}};
if (result.success) {
m_activeToolPath = result.toolPath;
fillToolboxMenu();
SetJsonItems();
}
} catch (const std::exception&) {
response = {{"success", false}, {"message", "Die Tool-Daten sind ungueltig."}};
}
const auto configPath = getToolConfigPath(m_activeToolId);
if (configPath.empty()) {
return;
}
m_configStore.setInt(configPath, "lua_timeout_seconds", seconds);
return response.dump();
}
std::string ToolboxWindow::getToolConfigIni() const {
if (m_activeToolId < 0) {
return {};
std::string ToolboxWindow::openActiveToolFolder() const {
if (!ToolRegistry::getToolDetails(userDirectory(), m_activeToolPath).success) {
return nlohmann::json({{"success", false}, {"message", "Der Toolordner wurde nicht gefunden."}}).dump();
}
const auto configPath = getToolConfigPath(m_activeToolId);
if (configPath.empty()) {
return {};
#ifdef _WIN32
const auto result = reinterpret_cast<std::intptr_t>(
ShellExecuteW(nullptr, L"open", m_activeToolPath.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result <= 32) {
return nlohmann::json({{"success", false}, {"message", "Der Toolordner konnte nicht geoeffnet werden."}}).dump();
}
return m_configStore.readAll(configPath);
}
void ToolboxWindow::setToolConfigIni(const std::string &content) {
if (m_activeToolId < 0) {
return;
}
const auto configPath = getToolConfigPath(m_activeToolId);
if (configPath.empty()) {
return;
}
m_configStore.writeAll(configPath, content);
}
std::string ToolboxWindow::getActiveToolName() {
if (m_activeToolId < 0 || m_activeToolId >= static_cast<int>(m_toolboxItems.size())) {
return {};
}
return m_toolboxItems[m_activeToolId].name;
#endif
return nlohmann::json({{"success", true}, {"message", "Toolordner wurde geoeffnet."}}).dump();
}

View File

@@ -6,6 +6,7 @@
#include <array>
#include <atomic>
#include <cstdio>
#include <mutex>
#include <thread>
#include "resource.h"
@@ -65,7 +66,10 @@ struct TrayController::Impl {
}} {}
bool initialize() {
activeController.store(this);
{
std::lock_guard<std::mutex> lock(activeControllerMutex);
activeController = this;
}
const HICON icon = toolbox_load_small_icon(GetModuleHandleW(nullptr), IDI_ICON1);
trayIcon = std::make_unique<toolboxTray>("toolbox.ico", "Toolbox");
@@ -84,26 +88,33 @@ struct TrayController::Impl {
}
void shutdown() {
stopping.store(true);
uninstallExplorerHook();
Impl* expected = this;
activeController.compare_exchange_strong(expected, nullptr);
{
std::lock_guard<std::mutex> activeLock(activeControllerMutex);
if (activeController == this) {
activeController = nullptr;
}
}
if (retryThread.joinable()) {
retryThread.request_stop();
retryThread.join();
}
std::lock_guard<std::mutex> trayLock(trayMutex);
trayIcon.reset();
}
static Impl* current() {
return activeController.load();
}
static void toggleToolboxWindow(tray_menu*) {
Impl* controller = current();
if (!controller) {
std::lock_guard<std::mutex> lock(activeControllerMutex);
if (!activeController || activeController->stopping.load()) {
return;
}
ToolboxWindow& window = controller->window;
if (window.m_bShow) {
ToolboxWindow& window = activeController->window;
if (window.isVisible()) {
window.hide();
return;
}
@@ -112,6 +123,10 @@ struct TrayController::Impl {
}
bool reinitTray() {
std::lock_guard<std::mutex> lock(trayMutex);
if (stopping.load()) {
return false;
}
if (!trayIcon || state.reinitScheduled) {
return false;
}
@@ -143,29 +158,50 @@ struct TrayController::Impl {
}
void retryTrayReinitAsync() {
std::thread([] {
if (retryThread.joinable()) {
if (retryRunning.load()) {
return;
}
retryThread.join();
}
retryRunning.store(true);
retryThread = std::jthread([this](std::stop_token stopToken) {
Sleep(500);
bool recovered = false;
for (int attempt = 0; attempt < 3; ++attempt) {
Impl* controller = current();
if (controller && isExplorerReady() && controller->reinitTray()) {
return;
if (stopToken.stop_requested() || stopping.load()) {
break;
}
if (isExplorerReady() && reinitTray()) {
recovered = true;
break;
}
Sleep(500 * (attempt + 1));
}
std::fprintf(stderr, "Tray konnte nach Explorer-Neustart nicht wiederhergestellt werden\n");
}).detach();
if (!recovered && !stopToken.stop_requested() && !stopping.load()) {
std::fprintf(stderr, "Tray konnte nach Explorer-Neustart nicht wiederhergestellt werden\n");
}
retryRunning.store(false);
});
}
void scheduleTrayReinit() {
const DWORD now = GetTickCount();
if (now - state.lastExplorerRestartTime < minExplorerRestartDelayMs) {
if (stopping.load()) {
return;
}
state.lastExplorerRestartTime = now;
{
std::lock_guard<std::mutex> lock(trayMutex);
const DWORD now = GetTickCount();
if (now - state.lastExplorerRestartTime < minExplorerRestartDelayMs) {
return;
}
state.lastExplorerRestartTime = now;
}
Sleep(300);
if (!reinitTray()) {
@@ -175,8 +211,8 @@ struct TrayController::Impl {
static void CALLBACK handleExplorerEvent(HWINEVENTHOOK, DWORD event, HWND hwnd, LONG idObject, LONG, DWORD,
DWORD) {
Impl* controller = current();
if (!controller || idObject != OBJID_WINDOW || !isTaskbarWindow(hwnd)) {
std::lock_guard<std::mutex> lock(activeControllerMutex);
if (!activeController || activeController->stopping.load() || idObject != OBJID_WINDOW || !isTaskbarWindow(hwnd)) {
return;
}
@@ -185,7 +221,7 @@ struct TrayController::Impl {
std::printf(isExplorerReady()
? "Explorer ist bereit - Tray wird neu initialisiert\n"
: "Explorer noch nicht bereit - Warten...\n");
controller->scheduleTrayReinit();
activeController->scheduleTrayReinit();
return;
}
@@ -218,10 +254,16 @@ struct TrayController::Impl {
std::array<tray_menu, 6> menuItems;
TrayState state;
HWINEVENTHOOK explorerHook = nullptr;
static std::atomic<Impl*> activeController;
std::jthread retryThread;
std::atomic_bool retryRunning = false;
std::atomic_bool stopping = false;
std::mutex trayMutex;
static std::mutex activeControllerMutex;
static Impl* activeController;
};
std::atomic<TrayController::Impl*> TrayController::Impl::activeController = nullptr;
std::mutex TrayController::Impl::activeControllerMutex;
TrayController::Impl* TrayController::Impl::activeController = nullptr;
TrayController::TrayController(ToolboxWindow& window)
: m_impl(std::make_unique<Impl>(window)) {}

138
tests/luaRunnerTests.cpp Normal file
View File

@@ -0,0 +1,138 @@
#include "luaRunner.h"
#include <cassert>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <future>
namespace {
bool runScript(LuaRunner& runner, const std::string& taskId, const std::filesystem::path& path) {
std::promise<bool> completion;
auto result = completion.get_future();
const auto started = runner.run(taskId, path, 5, [&completion](bool success) {
completion.set_value(success);
});
assert(started.started);
assert(result.wait_for(std::chrono::seconds(10)) == std::future_status::ready);
return result.get();
}
} // namespace
int main() {
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
const auto root = std::filesystem::temp_directory_path() /
std::filesystem::u8path("toolbox-unicode-工具-" + std::to_string(stamp));
std::filesystem::create_directories(root);
const auto validScript = root / std::filesystem::u8path("gültig.lua");
const auto invalidScript = root / std::filesystem::u8path("fehler.lua");
const auto longRunningScript = root / std::filesystem::u8path("langlaufend.lua");
const auto singleInstanceScript = root / std::filesystem::u8path("einzelinstanz.lua");
std::ofstream(validScript, std::ios::binary) << "local answer = 21 * 2\nassert(answer == 42)\n";
std::ofstream(invalidScript, std::ios::binary) << "error('expected failure')\n";
std::ofstream(longRunningScript, std::ios::binary) << "while true do end\n";
std::ofstream(singleInstanceScript, std::ios::binary)
<< "-- toolbox: single-instance\r\nwhile true do end\r\n";
LuaRunner runner;
assert(runScript(runner, "valid-tool", validScript));
assert(!runScript(runner, "invalid-tool", invalidScript));
std::promise<bool> longRunningCompletion;
auto longRunningResult = longRunningCompletion.get_future();
runner.run("C:/tools/first", longRunningScript, 0, [&longRunningCompletion](bool success) {
longRunningCompletion.set_value(success);
});
assert(runScript(runner, "C:/tools/second", validScript));
assert(longRunningResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
runner.cancel("C:/tools/first");
assert(longRunningResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
assert(!longRunningResult.get());
std::promise<bool> singleCompletion;
auto singleResult = singleCompletion.get_future();
const auto idleSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
assert(idleSingleStatus.singleInstance);
assert(!idleSingleStatus.running);
const auto firstSingle = runner.run(
"C:/tools/single", singleInstanceScript, 0,
[&singleCompletion](bool success) { singleCompletion.set_value(success); });
const auto duplicateSingle = runner.run(
"C:/tools/single", singleInstanceScript, 0, [](bool) {});
assert(firstSingle.started);
assert(firstSingle.singleInstance);
assert(!duplicateSingle.started);
assert(duplicateSingle.singleInstance);
const auto runningSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
assert(runningSingleStatus.singleInstance);
assert(runningSingleStatus.running);
runner.cancel("C:/tools/single");
assert(singleResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
assert(!singleResult.get());
const auto completedSingleStatus = runner.status("C:/tools/single", singleInstanceScript);
assert(completedSingleStatus.singleInstance);
assert(!completedSingleStatus.running);
std::promise<bool> firstParallelCompletion;
std::promise<bool> secondParallelCompletion;
auto firstParallelResult = firstParallelCompletion.get_future();
auto secondParallelResult = secondParallelCompletion.get_future();
const auto firstParallel = runner.run(
"C:/tools/parallel", longRunningScript, 0,
[&firstParallelCompletion](bool success) { firstParallelCompletion.set_value(success); });
const auto secondParallel = runner.run(
"C:/tools/parallel", longRunningScript, 0,
[&secondParallelCompletion](bool success) { secondParallelCompletion.set_value(success); });
assert(firstParallel.started);
assert(!firstParallel.singleInstance);
assert(secondParallel.started);
assert(!secondParallel.singleInstance);
assert(firstParallelResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
assert(secondParallelResult.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout);
runner.cancel("C:/tools/parallel");
assert(firstParallelResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
assert(secondParallelResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
assert(!firstParallelResult.get());
assert(!secondParallelResult.get());
const auto quickSingleScript = root / std::filesystem::u8path("kurze-einzelinstanz.lua");
std::ofstream(quickSingleScript, std::ios::binary)
<< "-- toolbox: single-instance\r\nreturn true\r\n";
std::promise<void> callbackEntered;
auto callbackEnteredResult = callbackEntered.get_future();
std::promise<void> releaseCallback;
auto releaseCallbackResult = releaseCallback.get_future().share();
std::promise<bool> quickSingleCompletion;
auto quickSingleResult = quickSingleCompletion.get_future();
const auto quickSingle = runner.run(
"C:/tools/quick-single", quickSingleScript, 0,
[&callbackEntered, releaseCallbackResult, &quickSingleCompletion](bool success) {
callbackEntered.set_value();
releaseCallbackResult.wait();
quickSingleCompletion.set_value(success);
});
assert(quickSingle.started);
assert(callbackEnteredResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
const auto statusDuringCallback = runner.status("C:/tools/quick-single", quickSingleScript);
auto duplicateDuringCallback = std::async(std::launch::async, [&runner, &quickSingleScript] {
return runner.run("C:/tools/quick-single", quickSingleScript, 0, [](bool) {});
});
const auto duplicateWait = duplicateDuringCallback.wait_for(std::chrono::milliseconds(100));
releaseCallback.set_value();
assert(duplicateDuringCallback.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
const auto duplicateDuringCallbackResult = duplicateDuringCallback.get();
assert(statusDuringCallback.singleInstance);
assert(!statusDuringCallback.running);
assert(duplicateWait == std::future_status::ready);
assert(!duplicateDuringCallbackResult.started);
assert(duplicateDuringCallbackResult.singleInstance);
assert(quickSingleResult.wait_for(std::chrono::seconds(5)) == std::future_status::ready);
assert(quickSingleResult.get());
runner.shutdown();
std::filesystem::remove_all(root);
return 0;
}

View File

@@ -0,0 +1,154 @@
#include "toolRegistry.h"
#include <cassert>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
namespace {
std::string readText(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
} // namespace
int main() {
const auto suffix = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const std::filesystem::path home = std::filesystem::temp_directory_path() /
("toolbox-settings-contract-" + suffix);
std::filesystem::create_directories(home);
ToolDraft created{
.name = "Acme",
.description = "Startet Acme.",
.luaScript = "error('must be replaced')",
.iconBase64 = {},
.timeoutSeconds = 30,
.templateType = "program",
.templateTarget = R"(C:\Program Files\Acme\Acme.exe)",
};
const auto creation = ToolRegistry::createTool(home, created);
assert(creation.success);
const auto oldPath = ToolRegistry::toolboxDirectory(home) / "Acme";
const auto toolboxRoot = ToolRegistry::toolboxDirectory(home);
const auto preparedStage = toolboxRoot / ".toolbox-update-101";
const auto preparedJournal = toolboxRoot / ".toolbox-transaction-101.json";
std::filesystem::copy(oldPath, preparedStage, std::filesystem::copy_options::recursive);
std::ofstream(preparedJournal) <<
R"({"version":1,"id":"101","originalName":"Acme","targetName":"Acme Website"})";
ToolRegistry::loadTools(home);
assert(std::filesystem::exists(oldPath));
assert(!std::filesystem::exists(preparedStage));
assert(!std::filesystem::exists(preparedJournal));
const auto movedStage = toolboxRoot / ".toolbox-update-102";
const auto movedBackup = toolboxRoot / ".toolbox-backup-102";
const auto movedJournal = toolboxRoot / ".toolbox-transaction-102.json";
std::filesystem::copy(oldPath, movedStage, std::filesystem::copy_options::recursive);
std::ofstream(movedJournal) <<
R"({"version":1,"id":"102","originalName":"Acme","targetName":"Acme Website"})";
std::filesystem::rename(oldPath, movedBackup);
ToolRegistry::loadTools(home);
assert(std::filesystem::exists(oldPath));
assert(!std::filesystem::exists(movedStage));
assert(!std::filesystem::exists(movedBackup));
assert(!std::filesystem::exists(movedJournal));
const auto mismatchedBackup = toolboxRoot / ".toolbox-backup-104";
const auto mismatchedJournal = toolboxRoot / ".toolbox-transaction-wrong.json";
std::filesystem::copy(oldPath, mismatchedBackup, std::filesystem::copy_options::recursive);
std::ofstream(mismatchedJournal) <<
R"({"version":1,"id":"104","originalName":"Acme","targetName":"Acme Website"})";
ToolRegistry::loadTools(home);
assert(std::filesystem::exists(mismatchedBackup));
assert(std::filesystem::exists(mismatchedJournal));
const auto outsideTool = home / "outside-tool";
std::filesystem::create_directory(outsideTool);
std::ofstream(outsideTool / "start.lua") << "print('outside')";
assert(!ToolRegistry::getToolDetails(home, outsideTool).success);
const auto orphanPath = ToolRegistry::toolboxDirectory(home) / ".toolbox-update-orphan";
std::filesystem::create_directory(orphanPath);
std::ofstream(orphanPath / "start.lua") << "print('orphan')";
const auto visibleTools = ToolRegistry::loadTools(home);
assert(visibleTools.size() == 1);
assert(visibleTools.front().name == "Acme");
std::ofstream(oldPath / "asset.txt") << "preserve me";
const auto loaded = ToolRegistry::getToolDetails(home, oldPath);
assert(loaded.success);
assert(loaded.details.name == "Acme");
assert(loaded.details.description == "Startet Acme.");
assert(loaded.details.timeoutSeconds == 30);
assert(loaded.details.templateType == "program");
assert(loaded.details.templateTarget == R"(C:\Program Files\Acme\Acme.exe)");
assert(loaded.details.luaScript.find("start") != std::string::npos);
assert(loaded.details.luaScript.find("must be replaced") == std::string::npos);
ToolUpdateDraft update{
.name = "Acme Website",
.description = "Öffnet die Acme-Website.",
.luaScript = "error('must also be replaced')",
.timeoutSeconds = 45,
.templateType = "website",
.templateTarget = "https://www.example.com/docs",
};
const auto updated = ToolRegistry::updateTool(home, oldPath, update);
assert(updated.success);
const auto newPath = ToolRegistry::toolboxDirectory(home) / "Acme Website";
assert(updated.toolPath == newPath);
assert(!std::filesystem::exists(oldPath));
assert(readText(newPath / "asset.txt") == "preserve me");
const auto committedBackup = toolboxRoot / ".toolbox-backup-103";
const auto committedJournal = toolboxRoot / ".toolbox-transaction-103.json";
std::filesystem::copy(newPath, committedBackup, std::filesystem::copy_options::recursive);
std::ofstream(committedJournal) <<
R"({"version":1,"id":"103","originalName":"Acme","targetName":"Acme Website"})";
ToolRegistry::loadTools(home);
assert(std::filesystem::exists(newPath));
assert(!std::filesystem::exists(committedBackup));
assert(!std::filesystem::exists(committedJournal));
const auto reloaded = ToolRegistry::getToolDetails(home, newPath);
assert(reloaded.success);
assert(reloaded.details.name == "Acme Website");
assert(reloaded.details.description == "Öffnet die Acme-Website.");
assert(reloaded.details.timeoutSeconds == 45);
assert(reloaded.details.templateType == "website");
assert(reloaded.details.templateTarget == "https://www.example.com/docs");
assert(reloaded.details.luaScript.find("https://www.example.com/docs") != std::string::npos);
assert(reloaded.details.luaScript.find("must also be replaced") == std::string::npos);
std::ofstream(newPath / "tool.json") << R"({"version":1,"type":"bogus","target":"ignored"})";
const auto invalidMetadata = ToolRegistry::getToolDetails(home, newPath);
assert(invalidMetadata.success);
assert(invalidMetadata.details.templateType == "lua");
assert(invalidMetadata.details.templateTarget.empty());
std::filesystem::remove(newPath / "tool.json");
const auto legacy = ToolRegistry::getToolDetails(home, newPath);
assert(legacy.success);
assert(legacy.details.templateType == "lua");
assert(legacy.details.templateTarget.empty());
const auto invalid = ToolRegistry::updateTool(home, newPath, ToolUpdateDraft{
.name = "../escape",
.description = "invalid",
.luaScript = "print('invalid')",
.timeoutSeconds = 30,
.templateType = "lua",
});
assert(!invalid.success);
assert(std::filesystem::exists(newPath));
assert(readText(newPath / "description.txt") == "Öffnet die Acme-Website.");
std::filesystem::remove_all(home);
return 0;
}

42
tests/toolRuntime.test.js Normal file
View File

@@ -0,0 +1,42 @@
'use strict'
const assert = require('node:assert/strict')
const runtime = require('../res/html/tool-runtime.js')
function fakeCard() {
const classes = new Set()
const status = { hidden: true }
return {
classList: {
add: value => classes.add(value),
remove: value => classes.delete(value),
toggle: (value, enabled) => enabled ? classes.add(value) : classes.delete(value),
contains: value => classes.has(value),
},
dataset: {},
attributes: {},
setAttribute(name, value) { this.attributes[name] = value },
querySelector(selector) {
return selector === '.tool-running-status' ? status : null
},
status,
}
}
assert.equal(runtime.initialRunning({ singleInstance: true, running: true }), true)
assert.equal(runtime.initialRunning({ singleInstance: false, running: true }), false)
const card = fakeCard()
runtime.applyCardState(card, true)
assert.equal(runtime.isCardRunBlocked(card), true)
assert.equal(card.classList.contains('tool-running'), true)
assert.equal(card.dataset.runDisabled, 'true')
assert.equal(card.attributes['aria-busy'], 'true')
assert.equal(card.status.hidden, false)
runtime.applyCardState(card, false)
assert.equal(runtime.isCardRunBlocked(card), false)
assert.equal(card.classList.contains('tool-running'), false)
assert.equal(card.dataset.runDisabled, 'false')
assert.equal(card.attributes['aria-busy'], 'false')
assert.equal(card.status.hidden, true)

View File

@@ -0,0 +1,95 @@
#include "toolTemplates.h"
#include <sol/sol.hpp>
#include <cassert>
#include <filesystem>
#include <string>
#include <tuple>
namespace {
std::tuple<std::string, std::string> capturedLaunch(const std::string& script) {
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::string);
std::string operation;
std::string target;
auto toolbox = lua.create_named_table("toolbox");
for (const std::string name : {"open_program", "open_folder", "open_url"}) {
toolbox.set_function(name, [&, name](const std::string& value) {
operation = name;
target = value;
return std::tuple{true, std::string{}};
});
}
const sol::protected_function_result result = lua.safe_script(script, sol::script_pass_on_error);
assert(result.valid());
return {operation, target};
}
std::string capturedPrint(const std::string& script) {
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::string);
std::string output;
lua.set_function("print", [&output](const std::string& value) { output = value; });
const sol::protected_function_result result = lua.safe_script(script, sol::script_pass_on_error);
assert(result.valid());
return output;
}
} // namespace
int main() {
const std::string syntaxSensitiveName = "x'); error('injected\n\t\\\"";
assert(capturedPrint(ToolTemplates::defaultLuaScript(syntaxSensitiveName)) == syntaxSensitiveName);
const std::filesystem::path programPath(R"(C:\Program Files\Acme\Acme.exe)");
const auto program = ToolTemplates::programLauncher(programPath);
assert(program.success);
assert(program.draft.name == "Acme");
assert(program.draft.description == "Startet Acme.");
assert(program.draft.luaScript.find(R"(C:\\Program Files\\Acme\\Acme.exe)") != std::string::npos);
assert(program.draft.luaScript.find("os.execute") == std::string::npos);
assert((capturedLaunch(program.draft.luaScript) ==
std::tuple{std::string("open_program"), program.draft.templateTarget}));
const std::filesystem::path folderPath(R"(C:\Users\User\Projects)");
const auto folder = ToolTemplates::folderLauncher(folderPath);
assert(folder.success);
assert(folder.draft.name == "Projects öffnen");
assert(folder.draft.description == "Öffnet den Ordner Projects.");
assert(folder.draft.luaScript.find(R"(C:\\Users\\User\\Projects)") != std::string::npos);
assert(folder.draft.luaScript.find("os.execute") == std::string::npos);
assert((capturedLaunch(folder.draft.luaScript) ==
std::tuple{std::string("open_folder"), folder.draft.templateTarget}));
const auto website = ToolTemplates::websiteLauncher("https://www.example.com/docs");
assert(website.success);
assert(website.draft.name == "example.com öffnen");
assert(website.draft.description == "Öffnet https://www.example.com/docs.");
assert(website.draft.luaScript.find("https://www.example.com/docs") != std::string::npos);
assert(website.draft.luaScript.find("os.execute") == std::string::npos);
assert((capturedLaunch(website.draft.luaScript) ==
std::tuple{std::string("open_url"), std::string("https://www.example.com/docs")}));
const std::string shellSensitiveUrl = "https://example.com/%TEMP%?a=1&b=(two)";
const auto shellSensitiveWebsite = ToolTemplates::websiteLauncher(shellSensitiveUrl);
assert(shellSensitiveWebsite.success);
assert((capturedLaunch(shellSensitiveWebsite.draft.luaScript) ==
std::tuple{std::string("open_url"), shellSensitiveUrl}));
const auto invalidWebsite = ToolTemplates::websiteLauncher("javascript:alert(1)");
assert(!invalidWebsite.success);
const auto invalidProgram = ToolTemplates::programLauncher(
std::filesystem::path(R"(C:\Temp\notes.txt)"));
assert(!invalidProgram.success);
const auto unicodeProgram = ToolTemplates::programLauncher(
std::filesystem::path(LR"(C:\Programme\工具.exe)"));
assert(unicodeProgram.success);
assert(unicodeProgram.draft.name == "工具");
assert(unicodeProgram.draft.luaScript.find("工具.exe") != std::string::npos);
return 0;
}

162
tests/toolbox_cli_test.py Normal file
View File

@@ -0,0 +1,162 @@
import json
import os
import pathlib
import subprocess
import sys
import tempfile
def run(cli: pathlib.Path, *args: str, expected_code: int = 0,
env: dict[str, str] | None = None) -> dict:
completed = subprocess.run(
[str(cli), *args],
check=False,
capture_output=True,
text=True,
encoding="utf-8",
env=env,
)
assert completed.returncode == expected_code, completed.stderr or completed.stdout
if expected_code == 0:
assert completed.stderr == "", completed.stderr
return json.loads(completed.stdout)
def run_text(cli: pathlib.Path, *args: str) -> str:
completed = subprocess.run(
[str(cli), *args],
check=False,
capture_output=True,
text=True,
encoding="utf-8",
)
assert completed.returncode == 0, completed.stderr or completed.stdout
assert completed.stderr == "", completed.stderr
return completed.stdout
def main() -> None:
cli = pathlib.Path(sys.argv[1])
assert cli.is_file(), f"CLI binary missing: {cli}"
short_help = run_text(cli, "-h")
long_help = run_text(cli, "--help")
for help_text in (short_help, long_help):
assert "TOOLBOX CLI" in help_text
assert "BEFEHLE" in help_text
assert "list" in help_text
assert "create" in help_text
assert "run" in help_text
assert not help_text.lstrip().startswith("{")
colored_help = run_text(cli, "--color", "always", "-h")
assert "\x1b[" in colored_help
with tempfile.TemporaryDirectory(prefix="toolbox-cli-test-工具-") as home:
created = run(
cli,
"--home", home,
"create",
"--name", "Agent 工具",
"--description", "Created through the CLI",
"--script", 'assert(type(toolbox.open_url) == "function")\nprint("agent-ready-✓")',
"--timeout", "5",
)
assert created == {
"message": "Tool wurde erstellt.",
"ok": True,
"tool": "Agent 工具",
}
listed = run(cli, "--home", home, "list")
assert listed["ok"] is True
assert listed["tools"] == [{
"description": "Created through the CLI",
"name": "Agent 工具",
}]
listed_with_trailing_home = run(cli, "list", "--home", home)
assert listed_with_trailing_home == listed
forced_color_listed = run(cli, "--color", "always", "--home", home, "list")
assert forced_color_listed == listed
executed = run(cli, "--home", home, "run", "Agent 工具")
assert executed == {
"ok": True,
"output": "agent-ready-✓\n",
"tool": "Agent 工具",
}
script_file = pathlib.Path(home) / "skript-工具.lua"
script_file.write_text('print("datei-✓")\n', encoding="utf-8")
file_created = run(
cli,
"--home", home,
"create",
"--name", "Datei 工具",
"--script-file", str(script_file),
)
assert file_created["ok"] is True
file_executed = run(cli, "--home", home, "run", "Datei 工具")
assert file_executed == {
"ok": True,
"output": "datei-✓\n",
"tool": "Datei 工具",
}
syntax_sensitive_name = "x'); error('injected"
injected = run(
cli,
"--home", home,
"create",
"--name", syntax_sensitive_name,
)
assert injected["ok"] is True
injected_run = run(cli, "--home", home, "run", syntax_sensitive_name)
assert injected_run == {
"ok": True,
"output": f"Hallo aus {syntax_sensitive_name}\n",
"tool": syntax_sensitive_name,
}
default_home_env = os.environ.copy()
default_home_env["USERPROFILE"] = home
default_listed = run(cli, "list", env=default_home_env)
assert {tool["name"] for tool in default_listed["tools"]} == {
"Agent 工具", "Datei 工具", syntax_sensitive_name
}
rejected = run(
cli,
"--home", home,
"create",
"--name", "../escape",
expected_code=2,
)
assert rejected["ok"] is False
assert not (pathlib.Path(home) / "escape").exists()
with tempfile.TemporaryDirectory(prefix="toolbox-cli-root-") as home, \
tempfile.TemporaryDirectory(prefix="toolbox-cli-external-") as external:
toolbox_root = pathlib.Path(home) / ".Toolbox"
if sys.platform == "win32":
linked = subprocess.run(
["cmd.exe", "/c", "mklink", "/J", str(toolbox_root), external],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
assert linked.returncode == 0
else:
toolbox_root.symlink_to(external, target_is_directory=True)
rejected_root = run(
cli,
"--home", home,
"create",
"--name", "Must Not Escape",
expected_code=2,
)
assert rejected_root["ok"] is False
assert not (pathlib.Path(external) / "Must Not Escape").exists()
if __name__ == "__main__":
main()