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:
25
.gitignore
vendored
25
.gitignore
vendored
@@ -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
6
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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
59
CONTRIBUTING.md
Normal 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
22
LICENSE
Normal 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/>.
|
||||
57
README.md
57
README.md
@@ -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.
|
||||
|
||||
@@ -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
42
THIRD_PARTY_LICENSES.md
Normal 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
400
cli/main.cpp
Normal 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
29
inc/luaHelpWindow.h
Normal 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();
|
||||
};
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
11
inc/toolLaunchBridge.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace sol {
|
||||
class state;
|
||||
}
|
||||
|
||||
namespace ToolLaunchBridge {
|
||||
|
||||
void registerFunctions(sol::state& lua);
|
||||
|
||||
} // namespace ToolLaunchBridge
|
||||
@@ -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
27
inc/toolTemplates.h
Normal 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
4
inc/toolboxCli.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
int runCli(std::vector<std::string> args);
|
||||
@@ -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);
|
||||
|
||||
@@ -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
1
lib/CLI11
Submodule
Submodule lib/CLI11 added at 37bb6edc53
1
lib/rang
Submodule
1
lib/rang
Submodule
Submodule lib/rang added at 56419fe334
49
main.cpp
49
main.cpp
@@ -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
347
res/html/add_tool.html
Normal 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>
|
||||
@@ -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 © 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 © 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
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>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">📁</button>
|
||||
</div>
|
||||
<button onclick="openEinstFirst()" class="button icon-button" id="option-button" style="display: none;">⚙</button>
|
||||
<button onclick="toggleGeneralSettings()" class="button icon-button" id="general-settings-button" title="Einstellungen">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main" id="toolbox">
|
||||
@@ -32,36 +33,54 @@
|
||||
<!--Schmidti.Digital © 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()">×</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>
|
||||
|
||||
347
res/html/lua_cheatsheet.html
Normal file
347
res/html/lua_cheatsheet.html
Normal 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 & 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: == ~= < > <= >=
|
||||
-- Logik: and, or, not</code></pre>
|
||||
</article>
|
||||
|
||||
<article class="lua-help-card">
|
||||
<h2>Bedingungen</h2>
|
||||
<pre><code class="language-lua">if count > 10 then
|
||||
print("groß")
|
||||
elseif count > 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 <= 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 & <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 & 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 & 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 <close> = 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 > 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 & 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>
|
||||
@@ -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
33
res/html/tool-runtime.js
Normal 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
21
res/vendor/prism/LICENSE.txt
vendored
Normal 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
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
1
res/vendor/prism/prism-lua.min.js
vendored
Normal 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
308
src/luaHelpWindow.cpp
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
60
src/toolLaunchBridge.cpp
Normal 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);
|
||||
}
|
||||
@@ -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
130
src/toolTemplates.cpp
Normal 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,
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
138
tests/luaRunnerTests.cpp
Normal 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;
|
||||
}
|
||||
154
tests/toolRegistrySettingsTests.cpp
Normal file
154
tests/toolRegistrySettingsTests.cpp
Normal 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
42
tests/toolRuntime.test.js
Normal 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)
|
||||
95
tests/toolTemplatesTests.cpp
Normal file
95
tests/toolTemplatesTests.cpp
Normal 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
162
tests/toolbox_cli_test.py
Normal 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()
|
||||
Reference in New Issue
Block a user