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:
+19
-6
@@ -1,8 +1,21 @@
|
|||||||
/.idea/
|
# Build output
|
||||||
/cmake-build-debug/
|
/cmake-build-*/
|
||||||
/cmake-build-debug-visual-studio/
|
|
||||||
/cmake-build-release-visual-studio/
|
|
||||||
/cmake-build-minsizerel-visual-studio/
|
|
||||||
/installer/.idea/
|
|
||||||
/out_embed/
|
/out_embed/
|
||||||
/embedded/
|
/embedded/
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
/.idea/
|
||||||
|
/installer/.idea/
|
||||||
|
/.vs/
|
||||||
|
/.vscode/
|
||||||
|
*.user
|
||||||
|
|
||||||
|
# AI assistants
|
||||||
|
/.claude/
|
||||||
|
/.junie/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
/.cache/
|
||||||
|
|
||||||
|
# Windows pseudo-files
|
||||||
|
/NUL
|
||||||
|
|||||||
@@ -19,3 +19,9 @@
|
|||||||
[submodule "lib/saucer4lua"]
|
[submodule "lib/saucer4lua"]
|
||||||
path = lib/saucer4lua
|
path = lib/saucer4lua
|
||||||
url = https://git.schmidti.digital/Yadciel/saucer4lua.git
|
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
|
||||||
|
|||||||
+75
-3
@@ -21,6 +21,9 @@ add_subdirectory(lib/saucer)
|
|||||||
add_subdirectory(lib/lua)
|
add_subdirectory(lib/lua)
|
||||||
add_subdirectory(lib/sol2)
|
add_subdirectory(lib/sol2)
|
||||||
add_subdirectory(lib/toolbox_win)
|
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)
|
# add_subdirectory(lib/saucer4lua)
|
||||||
|
|
||||||
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS
|
file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS
|
||||||
@@ -38,6 +41,7 @@ endif ()
|
|||||||
|
|
||||||
add_executable(Toolbox
|
add_executable(Toolbox
|
||||||
main.cpp
|
main.cpp
|
||||||
|
cli/main.cpp
|
||||||
${APP_SOURCES}
|
${APP_SOURCES}
|
||||||
${APP_HEADERS}
|
${APP_HEADERS}
|
||||||
)
|
)
|
||||||
@@ -55,6 +59,10 @@ target_include_directories(Toolbox
|
|||||||
lib/c_tray
|
lib/c_tray
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(Toolbox PRIVATE /utf-8)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_compile_definitions(Toolbox PRIVATE NOMINMAX UNICODE _UNICODE)
|
target_compile_definitions(Toolbox PRIVATE NOMINMAX UNICODE _UNICODE)
|
||||||
|
|
||||||
@@ -100,14 +108,78 @@ target_link_libraries(Toolbox
|
|||||||
tray
|
tray
|
||||||
nlohmann_json
|
nlohmann_json
|
||||||
cppcodec
|
cppcodec
|
||||||
|
CLI11::CLI11
|
||||||
|
rang
|
||||||
toolbox_win
|
toolbox_win
|
||||||
saucer::embedded
|
saucer::embedded
|
||||||
)
|
)
|
||||||
|
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_link_libraries(Toolbox PRIVATE shell32 user32 gdi32)
|
target_link_libraries(Toolbox PRIVATE shell32 user32 gdi32 shlwapi)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
|
include(CTest)
|
||||||
set_target_properties(Toolbox PROPERTIES WIN32_EXECUTABLE TRUE)
|
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 ()
|
endif ()
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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/>.
|
||||||
@@ -1,5 +1,49 @@
|
|||||||
# Toolbox
|
# 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
|
## install
|
||||||
|
|
||||||
first install node.js
|
first install node.js
|
||||||
@@ -16,4 +60,17 @@ like
|
|||||||
|
|
||||||
saucer embed D:/Repos/Toolbox/res/html D:/Repos/Toolbox/out_embed
|
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.
|
- Tests fuer Tool-Scanning ohne echte User-Home-Abhaengigkeit.
|
||||||
- Debug/Release-Smoke-Build lokal und spaeter in CI.
|
- Debug/Release-Smoke-Build lokal und spaeter in CI.
|
||||||
- Optional HTML-Lint oder DOM-Test fuer Renderfunktionen.
|
- 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.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
+35
-4
@@ -5,23 +5,54 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class LuaRunner {
|
class LuaRunner {
|
||||||
public:
|
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);
|
struct RunResult {
|
||||||
void cancel(int id);
|
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:
|
private:
|
||||||
struct LuaTask {
|
struct LuaTask {
|
||||||
std::shared_ptr<std::atomic_bool> cancel;
|
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 {
|
struct State {
|
||||||
std::mutex tasksMutex;
|
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>();
|
std::shared_ptr<State> m_state = std::make_shared<State>();
|
||||||
|
|||||||
+11
-10
@@ -13,15 +13,16 @@ class optionWindow {
|
|||||||
public:
|
public:
|
||||||
// Konstruktor erhält das Application-Objekt als Parameter
|
// Konstruktor erhält das Application-Objekt als Parameter
|
||||||
optionWindow(std::shared_ptr<saucer::application>& app,
|
optionWindow(std::shared_ptr<saucer::application>& app,
|
||||||
std::function<int()> getLuaTimeoutSeconds,
|
std::function<std::string()> getToolDetails,
|
||||||
std::function<void(int)> setLuaTimeoutSeconds,
|
std::function<std::string(const std::string &)> saveToolDetails,
|
||||||
std::function<std::string()> getActiveToolName,
|
std::function<std::string()> openToolFolder,
|
||||||
std::function<std::string()> getToolConfigIni,
|
std::function<std::string(const std::string &)> createTool,
|
||||||
std::function<void(const std::string &)> setToolConfigIni,
|
std::function<void()> openLuaHelp,
|
||||||
std::function<void()> openIndex);
|
std::function<void()> openIndex);
|
||||||
|
|
||||||
// Methode zum Anzeigen des Fensters
|
// Methode zum Anzeigen des Fensters
|
||||||
void show();
|
void show();
|
||||||
|
void showCreateTool();
|
||||||
|
|
||||||
// Für die Fensterinitialisierung
|
// Für die Fensterinitialisierung
|
||||||
void initialize();
|
void initialize();
|
||||||
@@ -33,11 +34,11 @@ private:
|
|||||||
saucer::smartview m_webview;
|
saucer::smartview m_webview;
|
||||||
std::unique_ptr<hwnd_module> m_hwnd_module;
|
std::unique_ptr<hwnd_module> m_hwnd_module;
|
||||||
HWND m_hwnd;
|
HWND m_hwnd;
|
||||||
std::function<int()> m_getLuaTimeoutSeconds;
|
std::function<std::string()> m_getToolDetails;
|
||||||
std::function<void(int)> m_setLuaTimeoutSeconds;
|
std::function<std::string(const std::string &)> m_saveToolDetails;
|
||||||
std::function<std::string()> m_getActiveToolName;
|
std::function<std::string()> m_openToolFolder;
|
||||||
std::function<std::string()> m_getToolConfigIni;
|
std::function<std::string(const std::string &)> m_createTool;
|
||||||
std::function<void(const std::string &)> m_setToolConfigIni;
|
std::function<void()> m_openLuaHelp;
|
||||||
std::function<void()> m_openIndex;
|
std::function<void()> m_openIndex;
|
||||||
|
|
||||||
void configureWindow();
|
void configureWindow();
|
||||||
|
|||||||
@@ -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
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -18,11 +19,73 @@ struct ToolboxItem {
|
|||||||
std::string license;
|
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 {
|
class ToolRegistry {
|
||||||
public:
|
public:
|
||||||
static std::filesystem::path toolboxDirectory(const std::filesystem::path& userPath);
|
static std::filesystem::path toolboxDirectory(const std::filesystem::path& userPath);
|
||||||
static bool ensureToolboxDirectory(const std::filesystem::path& userPath);
|
static bool ensureToolboxDirectory(const std::filesystem::path& userPath);
|
||||||
static std::vector<ToolboxItem> loadTools(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:
|
private:
|
||||||
static void encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item);
|
static void encodeIcon(const std::filesystem::path& imagePath, ToolboxItem& item);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
int runCli(std::vector<std::string> args);
|
||||||
@@ -9,19 +9,18 @@ class ToolboxJsBridge {
|
|||||||
public:
|
public:
|
||||||
struct Bindings {
|
struct Bindings {
|
||||||
std::function<std::string()> getToolboxItems;
|
std::function<std::string()> getToolboxItems;
|
||||||
std::function<void(int)> runLua;
|
std::function<std::string(const std::string&)> runLua;
|
||||||
std::function<void(int)> cancelLua;
|
std::function<void(const std::string&)> cancelLua;
|
||||||
std::function<void()> debugPrint;
|
std::function<void()> debugPrint;
|
||||||
std::function<void()> openFileSystem;
|
std::function<void()> openFileSystem;
|
||||||
std::function<void()> openCommandPrompt;
|
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()> openIndex;
|
||||||
std::function<void()> getNewContent;
|
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);
|
static void registerBindings(saucer::smartview& webview, Bindings bindings);
|
||||||
|
|||||||
+29
-20
@@ -5,6 +5,7 @@
|
|||||||
#include <saucer/webview.hpp>
|
#include <saucer/webview.hpp>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "luaRunner.h"
|
#include "luaRunner.h"
|
||||||
|
#include "luaHelpWindow.h"
|
||||||
#include "optionWindow.h"
|
#include "optionWindow.h"
|
||||||
#include "resourceLoader.h"
|
#include "resourceLoader.h"
|
||||||
#include "toolConfig.h"
|
#include "toolConfig.h"
|
||||||
@@ -18,19 +19,16 @@ public:
|
|||||||
explicit ToolboxWindow(std::shared_ptr<saucer::application>& app);
|
explicit ToolboxWindow(std::shared_ptr<saucer::application>& app);
|
||||||
~ToolboxWindow();
|
~ToolboxWindow();
|
||||||
|
|
||||||
bool m_bShow = false;
|
|
||||||
|
|
||||||
// Methode zum Anzeigen des Fensters
|
// Methode zum Anzeigen des Fensters
|
||||||
void show();
|
void show();
|
||||||
void hide();
|
void hide();
|
||||||
|
[[nodiscard]] bool isVisible() const noexcept;
|
||||||
// Für die Fensterinitialisierung
|
// Für die Fensterinitialisierung
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
void setupToolboxWindow();
|
void setupToolboxWindow();
|
||||||
void toggleDevTools();
|
void toggleDevTools();
|
||||||
|
|
||||||
std::atomic_bool isIndex = true;
|
|
||||||
|
|
||||||
std::vector<ToolboxItem> m_toolboxItems;
|
std::vector<ToolboxItem> m_toolboxItems;
|
||||||
|
|
||||||
saucer::smartview m_webview;
|
saucer::smartview m_webview;
|
||||||
@@ -40,20 +38,23 @@ private:
|
|||||||
HWND m_hwnd{};
|
HWND m_hwnd{};
|
||||||
std::shared_ptr<saucer::application> m_app;
|
std::shared_ptr<saucer::application> m_app;
|
||||||
std::unique_ptr<optionWindow> m_optionWindow;
|
std::unique_ptr<optionWindow> m_optionWindow;
|
||||||
|
std::unique_ptr<LuaHelpWindow> m_luaHelpWindow;
|
||||||
std::thread m_threadMonitor;
|
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::atomic<bool> m_runningMonitor = true;
|
||||||
std::chrono::steady_clock::time_point m_lastShowTime{};
|
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;
|
std::string jsonString;
|
||||||
|
|
||||||
#ifdef _WIN32
|
std::filesystem::path userPath = userDirectory();
|
||||||
std::filesystem::path userPath = std::getenv("USERPROFILE"); // Windows
|
|
||||||
#else
|
|
||||||
std::filesystem::path userPath = std::getenv("HOME"); // Unix/Linux/MacOS
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
static std::filesystem::path userDirectory();
|
||||||
void startThreadMonitor();
|
void startThreadMonitor();
|
||||||
|
void scheduleDelayedReposition();
|
||||||
|
|
||||||
bool serveHtmlFile(const std::string &htmlPath, bool setDecorations);
|
bool serveHtmlFile(const std::string &htmlPath, bool setDecorations);
|
||||||
|
|
||||||
@@ -69,8 +70,9 @@ private:
|
|||||||
|
|
||||||
void debug_print();
|
void debug_print();
|
||||||
|
|
||||||
void run_lua(int id);
|
LuaRunner::RunResult run_lua(int id);
|
||||||
void cancel_lua(int id);
|
std::string run_lua_path(const std::string& toolPath);
|
||||||
|
void cancel_lua(const std::string& toolPath);
|
||||||
|
|
||||||
void monitor_focus();
|
void monitor_focus();
|
||||||
|
|
||||||
@@ -78,10 +80,15 @@ private:
|
|||||||
|
|
||||||
void open_cmd();
|
void open_cmd();
|
||||||
|
|
||||||
|
void setWindowOpacity(int percent);
|
||||||
|
int getWindowOpacity() const;
|
||||||
|
|
||||||
void SetJsonItems();
|
void SetJsonItems();
|
||||||
|
|
||||||
void openEinst();
|
void openEinst();
|
||||||
void openEinst(int id);
|
void openEinst(const std::string& toolPath);
|
||||||
|
void openAddTool();
|
||||||
|
void openLuaHelp();
|
||||||
void centerWindow();
|
void centerWindow();
|
||||||
void resizeWindow(int width, int height);
|
void resizeWindow(int width, int height);
|
||||||
void resizeWindow(double widthPercent, double heightPercent);
|
void resizeWindow(double widthPercent, double heightPercent);
|
||||||
@@ -89,17 +96,19 @@ private:
|
|||||||
void openIndex();
|
void openIndex();
|
||||||
void getNewContent();
|
void getNewContent();
|
||||||
|
|
||||||
int getLuaTimeoutSeconds();
|
std::string getActiveToolDetails() const;
|
||||||
void setLuaTimeoutSeconds(int seconds);
|
std::string saveActiveToolDetails(const std::string &payload);
|
||||||
std::string getActiveToolName();
|
std::string openActiveToolFolder() const;
|
||||||
|
|
||||||
int m_activeToolId = -1;
|
std::filesystem::path m_activeToolPath;
|
||||||
LuaRunner m_luaRunner;
|
LuaRunner m_luaRunner;
|
||||||
ToolConfigStore m_configStore;
|
ToolConfigStore m_configStore;
|
||||||
|
int m_lastOpacity = 100;
|
||||||
|
|
||||||
std::filesystem::path getToolConfigPath(int id) const;
|
std::filesystem::path getToolConfigPath(int id) const;
|
||||||
void ensureConfigExists(int id) const;
|
|
||||||
int getLuaTimeoutSecondsForId(int id) const;
|
int getLuaTimeoutSecondsForId(int id) const;
|
||||||
std::string getToolConfigIni() const;
|
std::string createTool(const std::string &payload);
|
||||||
void setToolConfigIni(const std::string &content);
|
std::string deleteTool(const std::string& toolPath);
|
||||||
|
optionWindow& optionWindowInstance();
|
||||||
|
LuaHelpWindow& luaHelpWindowInstance();
|
||||||
};
|
};
|
||||||
|
|||||||
Submodule
+1
Submodule lib/CLI11 added at 37bb6edc53
Submodule
+1
Submodule lib/rang added at 56419fe334
@@ -1,11 +1,40 @@
|
|||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
#include <saucer/app.hpp>
|
#include <saucer/app.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "toolboxCli.h"
|
||||||
#include "toolboxWindow.h"
|
#include "toolboxWindow.h"
|
||||||
#include "trayController.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) {
|
coco::stray start(saucer::application* app) {
|
||||||
std::shared_ptr<saucer::application> appShared(app, [](saucer::application*) {});
|
std::shared_ptr<saucer::application> appShared(app, [](saucer::application*) {});
|
||||||
|
|
||||||
@@ -20,12 +49,18 @@ coco::stray start(saucer::application* app) {
|
|||||||
co_await app->finish();
|
co_await app->finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _DEBUG
|
int wmain(int argc, wchar_t** argv) {
|
||||||
int main(int, char**) {
|
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);
|
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
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+420
-123
@@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="settings-page">
|
<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-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-left" data-webview-resize="bl"></div>
|
||||||
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
|
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
|
||||||
|
|
||||||
<div class="titlebar" data-webview-drag>
|
<div class="titlebar" data-webview-drag>
|
||||||
<div class="titlebar-left" data-webview-drag>
|
<div class="titlebar-left" data-webview-drag>
|
||||||
<button class="titlebar-btn" onclick="goBack()" data-webview-ignore>←</button>
|
<button class="titlebar-btn" id="back-button" data-webview-ignore>←</button>
|
||||||
<span class="titlebar-title" data-webview-drag>Einstellungen</span>
|
<span class="titlebar-title" data-webview-drag>Tool bearbeiten</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions" data-webview-drag>
|
<div class="titlebar-actions" data-webview-drag>
|
||||||
<button class="titlebar-btn" data-webview-minimize data-webview-ignore>–</button>
|
<button class="titlebar-btn" data-webview-minimize data-webview-ignore>–</button>
|
||||||
@@ -27,149 +28,445 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-body">
|
<main class="tool-settings-page">
|
||||||
<!-- Sidebar für Optionen -->
|
<header class="tool-settings-heading">
|
||||||
<div class="sidebar">
|
<div id="settings-icon-preview" class="add-tool-icon-preview" aria-label="Tool-Icon">λ</div>
|
||||||
<h2>Einstellungen</h2>
|
<div>
|
||||||
<ul>
|
<span class="add-tool-eyebrow">Toolbox</span>
|
||||||
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
|
<h1 id="settings-heading">Tool bearbeiten</h1>
|
||||||
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
|
<p>Eigenschaften und Startverhalten dieses Tools.</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="general" id="general">
|
<nav class="tool-settings-tabs" aria-label="Einstellungsbereiche">
|
||||||
<div class="settings-card">
|
<button type="button" class="tool-settings-tab is-active" data-panel="general">Allgemein</button>
|
||||||
<h3 style="margin: 0 0 10px 0;">Tool</h3>
|
<button type="button" class="tool-settings-tab" data-panel="launch">Startverhalten</button>
|
||||||
<div id="tool-name" class="settings-tool-name">-</div>
|
<button type="button" class="tool-settings-tab" data-panel="actions">Aktionen</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
|
<form id="tool-settings-form" class="tool-settings-form">
|
||||||
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
|
<section class="tool-settings-panel is-active" data-panel-content="general">
|
||||||
<div class="settings-help">
|
<div class="add-tool-section">
|
||||||
0 = kein Timeout
|
<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>
|
</div>
|
||||||
|
|
||||||
<button id="save-settings" class="back-button settings-save">Speichern</button>
|
<div id="native-target-controls" class="tool-settings-target" hidden>
|
||||||
<div id="save-status" class="settings-status"></div>
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
<div id="lua-section" class="add-tool-section">
|
||||||
<div class="footer">
|
<div class="add-tool-section-heading">
|
||||||
<!--Schmidti.Digital © 2024-->
|
<h2>start.lua</h2>
|
||||||
</div>
|
<button type="button" class="lua-help-trigger" id="lua-help-button">Lua-Cheatsheet</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
|
||||||
|
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
|
||||||
<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();
|
saucer.exposed.openIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showContent(option) {
|
function highlightLua() {
|
||||||
const general = document.getElementById('general');
|
scriptHighlight.textContent = scriptInput.value + (scriptInput.value.endsWith('\n') ? ' ' : '');
|
||||||
const editor = document.getElementById('editor');
|
Prism.highlightElement(scriptHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
if (option === 'editor') {
|
function renderIcon() {
|
||||||
editor.style.display = 'flex';
|
iconPreview.replaceChildren();
|
||||||
general.style.display = 'none';
|
if (!iconBase64) {
|
||||||
} else if (option === 'general') {
|
iconPreview.textContent = 'λ';
|
||||||
general.style.display = 'flex';
|
return;
|
||||||
editor.style.display = 'none';
|
}
|
||||||
} else {
|
const image = document.createElement('img');
|
||||||
editor.style.display = 'none';
|
image.src = `data:image/png;base64,${iconBase64}`;
|
||||||
general.style.display = 'none';
|
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', () => {
|
async function applyWebsite() {
|
||||||
const editor = document.getElementById('code-editor');
|
setStatus('Website wird geprüft…', 'pending');
|
||||||
const output = document.getElementById('highlightedOutput');
|
try {
|
||||||
showContent('general');
|
applyTemplateResponse(await saucer.exposed.createWebsiteTemplate(websiteInput.value.trim()));
|
||||||
|
} catch (error) {
|
||||||
function escapeHtml(text) {
|
console.error('Website-Prüfung fehlgeschlagen:', error);
|
||||||
return text
|
setStatus('Die Website konnte nicht übernommen werden.', 'error');
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function highlightLua(code) {
|
function showPanel(name) {
|
||||||
let escaped = escapeHtml(code);
|
document.querySelectorAll('[data-panel-content]').forEach(panel => {
|
||||||
const tokens = [];
|
panel.classList.toggle('is-active', panel.dataset.panelContent === name);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-panel]').forEach(tab => {
|
||||||
const toolName = document.getElementById('tool-name');
|
tab.classList.toggle('is-active', tab.dataset.panel === 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.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+203
-131
@@ -20,6 +20,7 @@
|
|||||||
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">📁</button>
|
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">📁</button>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="openEinstFirst()" class="button icon-button" id="option-button" style="display: none;">⚙</button>
|
<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>
|
</div>
|
||||||
<div class="main" id="toolbox">
|
<div class="main" id="toolbox">
|
||||||
@@ -32,36 +33,54 @@
|
|||||||
<!--Schmidti.Digital © 2024-->
|
<!--Schmidti.Digital © 2024-->
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
function getNewContent(){
|
function getNewContent(){
|
||||||
saucer.exposed.getNewContent();
|
saucer.exposed.getNewContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function runLuaWithId(event) {
|
async function runLuaWithId(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.target.tagName === 'BUTTON') {
|
if (event.target.closest('button')) {
|
||||||
// Falls der Button gedrückt wurde, brich die Funktion ab
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const button = event.target;
|
|
||||||
|
|
||||||
const toolCard = button.closest('.tool-card');
|
const toolCard = event.currentTarget;
|
||||||
if (toolCard) {
|
if (toolCard) {
|
||||||
const toolId = toolCard.getAttribute('data-tool-id');
|
const toolId = toolCard.getAttribute('data-tool-id');
|
||||||
|
const toolKey = toolCard.dataset.toolKey;
|
||||||
|
|
||||||
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) !== 'true') {
|
if (ToolRuntime.isCardRunBlocked(toolCard)) {
|
||||||
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 {
|
|
||||||
console.log(`Button is already disabled for tool ID: ${toolId}`);
|
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() {
|
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() {
|
function reloadContent() {
|
||||||
const toolbox = document.getElementById('toolbox');
|
loadToolboxItems();
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableButtonByToolId(toolId) {
|
function setToolRunningByPath(toolKey, running) {
|
||||||
saucer.exposed.debug_print();
|
const card = Array.from(document.querySelectorAll('.tool-card'))
|
||||||
// Finden Sie den Button mit dem entsprechenden data-tool-id
|
.find(candidate => candidate.dataset.toolKey === toolKey);
|
||||||
const button = document.querySelector(`.tool-card[data-tool-id='${toolId}']`);
|
ToolRuntime.applyCardState(card, running);
|
||||||
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 openEinst(event, toolId){
|
function openEinst(event, toolPath){
|
||||||
if (event) {
|
if (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
console.log('Einst Button geklickt!');
|
console.log('Einst Button geklickt!');
|
||||||
saucer.exposed.openEinst(toolId);
|
saucer.exposed.openEinst(toolPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEinstFirst(){
|
function openEinstFirst(){
|
||||||
@@ -163,11 +192,100 @@
|
|||||||
if (!firstCard) {
|
if (!firstCard) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toolId = firstCard.getAttribute('data-tool-id');
|
saucer.exposed.openEinst(firstCard.dataset.toolKey);
|
||||||
saucer.exposed.openEinst(parseInt(toolId));
|
}
|
||||||
|
|
||||||
|
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', () => {
|
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) => {
|
document.addEventListener('click', (e) => {
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (link) {
|
if (link) {
|
||||||
@@ -175,53 +293,7 @@
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
const toolbox = document.getElementById('toolbox');
|
loadToolboxItems();
|
||||||
|
|
||||||
// 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));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -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>
|
||||||
+939
-15
@@ -15,6 +15,32 @@
|
|||||||
--accent: #4fc3ff;
|
--accent: #4fc3ff;
|
||||||
--accent-deep: #1e74c9;
|
--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 {
|
html {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -33,7 +59,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: rgba(10, 26, 40, 0.80);
|
background: rgba(10, 26, 40, 1.0);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -127,20 +153,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-y: auto;
|
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 {
|
.tool-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -161,6 +173,14 @@
|
|||||||
background: var(--glass-strong);
|
background: var(--glass-strong);
|
||||||
box-shadow: 0 16px 26px var(--shadow), inset 0 1px 0 var(--shine);
|
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 {
|
.tool-card img {
|
||||||
width: 62px;
|
width: 62px;
|
||||||
height: 62px;
|
height: 62px;
|
||||||
@@ -185,8 +205,16 @@
|
|||||||
color: #9bc4dc;
|
color: #9bc4dc;
|
||||||
}
|
}
|
||||||
.tool-action {
|
.tool-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
.tool-running-status {
|
||||||
|
color: #ffb7bd;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
.tool-action button {
|
.tool-action button {
|
||||||
background: linear-gradient(180deg, rgba(130, 220, 255, 0.9), rgba(40, 120, 190, 0.95));
|
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);
|
border: 1px solid rgba(140, 210, 255, 0.6);
|
||||||
@@ -521,3 +549,899 @@
|
|||||||
.settings-page .settings-save {
|
.settings-page .settings-save {
|
||||||
margin-top: 16px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}));
|
||||||
Vendored
+21
@@ -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.
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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:/[\[\](){},;]|\.+|:+/};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+140
-25
@@ -1,9 +1,14 @@
|
|||||||
#include "luaRunner.h"
|
#include "luaRunner.h"
|
||||||
|
#include "toolLaunchBridge.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <ranges>
|
||||||
|
#include <sstream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
|
||||||
#include <sol/sol.hpp>
|
#include <sol/sol.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -21,7 +26,7 @@ struct LuaHookContext {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void luaCancelHook(lua_State* hookState, lua_Debug*) {
|
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) {
|
if (!hookCtx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,17 +48,71 @@ std::chrono::steady_clock::time_point deadlineFromTimeout(int timeoutSeconds) {
|
|||||||
return std::chrono::steady_clock::now() + std::chrono::seconds(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
|
} // namespace
|
||||||
|
|
||||||
void LuaRunner::run(int id, const std::filesystem::path& scriptPath, int timeoutSeconds, CompletionCallback onComplete) {
|
LuaRunner::~LuaRunner() {
|
||||||
auto cancelFlag = std::make_shared<std::atomic_bool>(false);
|
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 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);
|
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;
|
sol::state lua;
|
||||||
lua.open_libraries(sol::lib::base,
|
lua.open_libraries(sol::lib::base,
|
||||||
sol::lib::os,
|
sol::lib::os,
|
||||||
@@ -65,40 +124,96 @@ void LuaRunner::run(int id, const std::filesystem::path& scriptPath, int timeout
|
|||||||
sol::lib::utf8,
|
sol::lib::utf8,
|
||||||
sol::lib::io);
|
sol::lib::io);
|
||||||
|
|
||||||
LuaHookContext ctx{
|
ToolLaunchBridge::registerFunctions(lua);
|
||||||
cancelFlag,
|
|
||||||
deadlineFromTimeout(timeoutSeconds)
|
LuaHookContext ctx{cancelFlag, deadlineFromTimeout(timeoutSeconds)};
|
||||||
};
|
|
||||||
|
|
||||||
lua_State* L = lua.lua_state();
|
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);
|
lua_sethook(L, luaCancelHook, LUA_MASKCOUNT, hookInstructionCount);
|
||||||
|
|
||||||
|
bool succeeded = false;
|
||||||
try {
|
try {
|
||||||
std::cout << "Running Lua script: " << scriptPath.string() << std::endl;
|
if (stopToken.stop_requested()) {
|
||||||
lua.script_file(scriptPath.string());
|
cancelFlag->store(true, std::memory_order_relaxed);
|
||||||
} catch (const sol::error& e) {
|
}
|
||||||
|
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::cerr << "Lua error: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
executionFinishedFlag->store(true, std::memory_order_release);
|
||||||
std::lock_guard<std::mutex> lock(state->tasksMutex);
|
|
||||||
state->tasks.erase(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete();
|
onComplete(succeeded);
|
||||||
}
|
}
|
||||||
}).detach();
|
callbackFinishedFlag->store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "Lua-Skript wurde gestartet." << std::endl;
|
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;
|
auto state = m_state;
|
||||||
std::lock_guard<std::mutex> lock(state->tasksMutex);
|
std::lock_guard<std::mutex> lock(state->tasksMutex);
|
||||||
auto it = state->tasks.find(id);
|
auto it = state->tasks.find(taskId);
|
||||||
if (it != state->tasks.end() && it->second.cancel) {
|
if (it != state->tasks.end()) {
|
||||||
it->second.cancel->store(true, std::memory_order_relaxed);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-36
@@ -1,12 +1,98 @@
|
|||||||
#include "optionWindow.h"
|
#include "optionWindow.h"
|
||||||
|
#include "toolTemplates.h"
|
||||||
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <shobjidl.h>
|
||||||
|
#include <wrl/client.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include <saucer/embedded/all.hpp>
|
#include <saucer/embedded/all.hpp>
|
||||||
#include <saucer/navigation.hpp>
|
#include <saucer/navigation.hpp>
|
||||||
#include <saucer/app.hpp>
|
#include <saucer/app.hpp>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace {
|
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;
|
std::unordered_map<HWND, WNDPROC> g_option_prev_procs;
|
||||||
|
|
||||||
LRESULT CALLBACK option_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
|
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,
|
optionWindow::optionWindow(std::shared_ptr<saucer::application>& app,
|
||||||
std::function<int()> getLuaTimeoutSeconds,
|
std::function<std::string()> getToolDetails,
|
||||||
std::function<void(int)> setLuaTimeoutSeconds,
|
std::function<std::string(const std::string &)> saveToolDetails,
|
||||||
std::function<std::string()> getActiveToolName,
|
std::function<std::string()> openToolFolder,
|
||||||
std::function<std::string()> getToolConfigIni,
|
std::function<std::string(const std::string &)> createTool,
|
||||||
std::function<void(const std::string &)> setToolConfigIni,
|
std::function<void()> openLuaHelp,
|
||||||
std::function<void()> openIndex)
|
std::function<void()> openIndex)
|
||||||
: m_app(app)
|
: m_app(app)
|
||||||
, m_webview([&]() -> saucer::smartview {
|
, 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");
|
throw std::runtime_error("optionWindow: failed to create saucer::window");
|
||||||
}
|
}
|
||||||
saucer::webview::options opts{window_res.value()};
|
saucer::webview::options opts{window_res.value()};
|
||||||
|
opts.storage_path = std::filesystem::temp_directory_path() / "toolbox-option-webview2";
|
||||||
opts.browser_flags.insert("--disable-gpu");
|
opts.browser_flags.insert("--disable-gpu");
|
||||||
auto sv_res = saucer::smartview::create(opts);
|
auto sv_res = saucer::smartview::create(opts);
|
||||||
if (!sv_res.has_value()) {
|
if (!sv_res.has_value()) {
|
||||||
@@ -60,11 +147,11 @@ optionWindow::optionWindow(std::shared_ptr<saucer::application>& app,
|
|||||||
return std::move(sv_res.value());
|
return std::move(sv_res.value());
|
||||||
}())
|
}())
|
||||||
, m_hwnd()
|
, m_hwnd()
|
||||||
, m_getLuaTimeoutSeconds(std::move(getLuaTimeoutSeconds))
|
, m_getToolDetails(std::move(getToolDetails))
|
||||||
, m_setLuaTimeoutSeconds(std::move(setLuaTimeoutSeconds))
|
, m_saveToolDetails(std::move(saveToolDetails))
|
||||||
, m_getActiveToolName(std::move(getActiveToolName))
|
, m_openToolFolder(std::move(openToolFolder))
|
||||||
, m_getToolConfigIni(std::move(getToolConfigIni))
|
, m_createTool(std::move(createTool))
|
||||||
, m_setToolConfigIni(std::move(setToolConfigIni))
|
, m_openLuaHelp(std::move(openLuaHelp))
|
||||||
, m_openIndex(std::move(openIndex))
|
, m_openIndex(std::move(openIndex))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -110,6 +197,12 @@ void optionWindow::configureWindow() {
|
|||||||
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
|
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
|
||||||
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
|
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 windowWidth = static_cast<int>(screenWidth * (500.0 / 1920.0));
|
||||||
int windowHeight = static_cast<int>(screenHeight * (700.0 / 1080.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_background(bg);
|
||||||
m_webview.parent().set_decorations(saucer::window::decoration::none);
|
m_webview.parent().set_decorations(saucer::window::decoration::none);
|
||||||
|
|
||||||
int xPos = (screenWidth - windowWidth) / 2;
|
int xPos = workArea.left + (screenWidth - windowWidth) / 2;
|
||||||
int yPos = (screenHeight - windowHeight) / 2;
|
int yPos = workArea.top + (screenHeight - windowHeight) / 2;
|
||||||
|
|
||||||
// HWND über das Modul abrufen
|
|
||||||
if (HWND hwnd = m_hwnd) {
|
if (HWND hwnd = m_hwnd) {
|
||||||
SetWindowPos(hwnd, nullptr, xPos, yPos, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
|
SetWindowPos(hwnd, nullptr, xPos, yPos, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
|
||||||
}
|
}
|
||||||
@@ -164,9 +256,17 @@ void optionWindow::runLuaScript() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void optionWindow::show() {
|
void optionWindow::show() {
|
||||||
m_webview.parent().show();
|
m_webview.serve("/html/einstellungen_custom.html");
|
||||||
runLuaScript();
|
|
||||||
configureWindow();
|
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() {
|
void optionWindow::exposeToJs() {
|
||||||
@@ -177,36 +277,43 @@ void optionWindow::exposeToJs() {
|
|||||||
m_webview.parent().hide();
|
m_webview.parent().hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
m_webview.expose("getLuaTimeoutSeconds", [&]() -> int {
|
m_webview.expose("getToolDetails", [&]() -> std::string {
|
||||||
if (m_getLuaTimeoutSeconds) {
|
return m_getToolDetails ? m_getToolDetails() :
|
||||||
return m_getLuaTimeoutSeconds();
|
R"({"success":false,"message":"Kein Tool ist ausgewaehlt."})";
|
||||||
}
|
|
||||||
return 30;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
m_webview.expose("setLuaTimeoutSeconds", [&](int seconds) {
|
m_webview.expose("saveToolDetails", [&](const std::string& payload) -> std::string {
|
||||||
if (m_setLuaTimeoutSeconds) {
|
return m_saveToolDetails ? m_saveToolDetails(payload) :
|
||||||
m_setLuaTimeoutSeconds(seconds);
|
R"({"success":false,"message":"Das Tool kann nicht gespeichert werden."})";
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
m_webview.expose("getActiveToolName", [&]() -> std::string {
|
m_webview.expose("openToolFolder", [&]() -> std::string {
|
||||||
if (m_getActiveToolName) {
|
return m_openToolFolder ? m_openToolFolder() :
|
||||||
return m_getActiveToolName();
|
R"({"success":false,"message":"Der Toolordner kann nicht geoeffnet werden."})";
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
m_webview.expose("getToolConfigIni", [&]() -> std::string {
|
m_webview.expose("createTool", [&](const std::string &payload) -> std::string {
|
||||||
if (m_getToolConfigIni) {
|
if (m_createTool) {
|
||||||
return m_getToolConfigIni();
|
return m_createTool(payload);
|
||||||
}
|
}
|
||||||
return {};
|
return R"({"success":false,"message":"Tool-Erstellung ist nicht verfuegbar."})";
|
||||||
});
|
});
|
||||||
|
|
||||||
m_webview.expose("setToolConfigIni", [&](const std::string &content) {
|
m_webview.expose("selectProgramTemplate", [&]() -> std::string {
|
||||||
if (m_setToolConfigIni) {
|
return selectionJson(selectPath(m_hwnd, false), &ToolTemplates::programLauncher);
|
||||||
m_setToolConfigIni(content);
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+485
-6
@@ -1,9 +1,303 @@
|
|||||||
#include "toolRegistry.h"
|
#include "toolRegistry.h"
|
||||||
|
#include "toolTemplates.h"
|
||||||
|
|
||||||
#include <cppcodec/base64_rfc4648.hpp>
|
#include <cppcodec/base64_rfc4648.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#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) {
|
std::filesystem::path ToolRegistry::toolboxDirectory(const std::filesystem::path& userPath) {
|
||||||
return userPath / ".Toolbox";
|
return userPath / ".Toolbox";
|
||||||
@@ -12,12 +306,11 @@ std::filesystem::path ToolRegistry::toolboxDirectory(const std::filesystem::path
|
|||||||
bool ToolRegistry::ensureToolboxDirectory(const std::filesystem::path& userPath) {
|
bool ToolRegistry::ensureToolboxDirectory(const std::filesystem::path& userPath) {
|
||||||
const std::filesystem::path toolboxPath = toolboxDirectory(userPath);
|
const std::filesystem::path toolboxPath = toolboxDirectory(userPath);
|
||||||
if (std::filesystem::exists(toolboxPath)) {
|
if (std::filesystem::exists(toolboxPath)) {
|
||||||
return true;
|
return std::filesystem::is_directory(toolboxPath) && !isReparsePoint(toolboxPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (std::filesystem::create_directory(toolboxPath)) {
|
if (std::filesystem::create_directory(toolboxPath)) {
|
||||||
std::cout << ".Toolbox Verzeichnis wurde erstellt." << std::endl;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,21 +326,31 @@ std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& us
|
|||||||
std::vector<ToolboxItem> items;
|
std::vector<ToolboxItem> items;
|
||||||
const std::filesystem::path toolboxDir = toolboxDirectory(userPath);
|
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;
|
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recoverTransactions(toolboxDir);
|
||||||
|
|
||||||
int id = 0;
|
int id = 0;
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(toolboxDir)) {
|
for (const auto& entry : std::filesystem::directory_iterator(toolboxDir)) {
|
||||||
if (!entry.is_directory()) {
|
if (!entry.is_directory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const std::string entryName = pathUtf8(entry.path().filename());
|
||||||
|
if (isInternalEntryName(entryName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isDirectToolPath(userPath, entry.path())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ToolboxItem item;
|
ToolboxItem item;
|
||||||
item.id = std::to_string(id++);
|
item.id = std::to_string(id++);
|
||||||
item.name = entry.path().filename().string();
|
item.name = entryName;
|
||||||
item.path = entry.path().string();
|
item.path = pathUtf8(entry.path());
|
||||||
|
|
||||||
encodeIcon(entry.path() / "icon.png", item);
|
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";
|
const std::filesystem::path luaScript = entry.path() / "start.lua";
|
||||||
if (std::filesystem::exists(luaScript)) {
|
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));
|
items.push_back(std::move(item));
|
||||||
@@ -68,7 +371,183 @@ std::vector<ToolboxItem> ToolRegistry::loadTools(const std::filesystem::path& us
|
|||||||
return items;
|
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) {
|
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);
|
std::ifstream file(imagePath, std::ios::binary | std::ios::ate);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
std::cerr << "Fehler beim Oeffnen des PNG-Bildes: " << imagePath << std::endl;
|
std::cerr << "Fehler beim Oeffnen des PNG-Bildes: " << imagePath << std::endl;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+22
-26
@@ -5,12 +5,12 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
|
|||||||
return bindings.getToolboxItems();
|
return bindings.getToolboxItems();
|
||||||
});
|
});
|
||||||
|
|
||||||
webview.expose("run_lua", [bindings](int id) {
|
webview.expose("run_lua", [bindings](const std::string& toolPath) -> std::string {
|
||||||
bindings.runLua(id);
|
return bindings.runLua(toolPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
webview.expose("cancel_lua", [bindings](int id) {
|
webview.expose("cancel_lua", [bindings](const std::string& toolPath) {
|
||||||
bindings.cancelLua(id);
|
bindings.cancelLua(toolPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
webview.expose("debug_print", [bindings]() {
|
webview.expose("debug_print", [bindings]() {
|
||||||
@@ -25,8 +25,24 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
|
|||||||
bindings.openCommandPrompt();
|
bindings.openCommandPrompt();
|
||||||
});
|
});
|
||||||
|
|
||||||
webview.expose("openEinst", [bindings](int id) {
|
webview.expose("openAddTool", [bindings]() {
|
||||||
bindings.openSettings(id);
|
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]() {
|
webview.expose("openIndex", [bindings]() {
|
||||||
@@ -36,24 +52,4 @@ void ToolboxJsBridge::registerBindings(saucer::smartview& webview, Bindings bind
|
|||||||
webview.expose("getNewContent", [bindings]() {
|
webview.expose("getNewContent", [bindings]() {
|
||||||
bindings.getNewContent();
|
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
+285
-99
@@ -1,3 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#include "toolboxWindow.h"
|
#include "toolboxWindow.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@@ -12,6 +20,7 @@
|
|||||||
#undef min
|
#undef min
|
||||||
#endif
|
#endif
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <saucer/navigation.hpp>
|
#include <saucer/navigation.hpp>
|
||||||
#include <saucer/app.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)
|
ToolboxWindow::ToolboxWindow(std::shared_ptr<saucer::application>& app)
|
||||||
: m_webview([&]() -> saucer::smartview {
|
: m_webview([&]() -> saucer::smartview {
|
||||||
auto window_res = saucer::window::create(app.get());
|
auto window_res = saucer::window::create(app.get());
|
||||||
@@ -129,9 +156,9 @@ void ToolboxWindow::loadResources() {
|
|||||||
|
|
||||||
void ToolboxWindow::show() {
|
void ToolboxWindow::show() {
|
||||||
m_webview.parent().show();
|
m_webview.parent().show();
|
||||||
m_bShow = true;
|
m_isVisible.store(true, std::memory_order_release);
|
||||||
m_lastShowTime = std::chrono::steady_clock::now();
|
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.
|
// In some cases the HWND/frame metrics are finalized only after showing.
|
||||||
// Re-acquire HWND and reposition now, plus schedule a short delayed reposition.
|
// Re-acquire HWND and reposition now, plus schedule a short delayed reposition.
|
||||||
@@ -151,33 +178,51 @@ void ToolboxWindow::show() {
|
|||||||
configureWindow();
|
configureWindow();
|
||||||
|
|
||||||
// Deferred reposition to catch late layouting from the framework
|
// 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));
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||||
configureWindow();
|
if (!stopToken.stop_requested()) {
|
||||||
}).detach();
|
configureWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToolboxWindow::hide() {
|
void ToolboxWindow::hide() {
|
||||||
m_webview.parent().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() {
|
void ToolboxWindow::ExposeToJs() {
|
||||||
ToolboxJsBridge::registerBindings(m_webview, {
|
ToolboxJsBridge::registerBindings(m_webview, {
|
||||||
.getToolboxItems = [this]() { return jsonString; },
|
.getToolboxItems = [this]() {
|
||||||
.runLua = [this](int id) { run_lua(id); },
|
SetJsonItems();
|
||||||
.cancelLua = [this](int id) { cancel_lua(id); },
|
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(); },
|
.debugPrint = [this]() { debug_print(); },
|
||||||
.openFileSystem = [this]() { open_file_system(); },
|
.openFileSystem = [this]() { open_file_system(); },
|
||||||
.openCommandPrompt = [this]() { open_cmd(); },
|
.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(); },
|
.openIndex = [this]() { openIndex(); },
|
||||||
.getNewContent = [this]() { getNewContent(); },
|
.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();
|
nlohmann::json jsonArray = nlohmann::json::array();
|
||||||
|
|
||||||
for (const auto& item : m_toolboxItems) {
|
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 = {
|
nlohmann::json jsonItem = {
|
||||||
{"id", item.id},
|
{"id", item.id},
|
||||||
{"name", item.name},
|
{"name", item.name},
|
||||||
@@ -195,7 +247,9 @@ void ToolboxWindow::SetJsonItems() {// Konvertierung des Vektors in JSON
|
|||||||
{"version", item.version},
|
{"version", item.version},
|
||||||
{"author", item.author},
|
{"author", item.author},
|
||||||
{"url", item.url},
|
{"url", item.url},
|
||||||
{"license", item.license}
|
{"license", item.license},
|
||||||
|
{"singleInstance", taskStatus.singleInstance},
|
||||||
|
{"running", taskStatus.singleInstance && taskStatus.running}
|
||||||
};
|
};
|
||||||
jsonArray.push_back(jsonItem);
|
jsonArray.push_back(jsonItem);
|
||||||
}
|
}
|
||||||
@@ -213,26 +267,62 @@ void ToolboxWindow::fillToolboxMenu() {
|
|||||||
m_toolboxItems = ToolRegistry::loadTools(userPath);
|
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())) {
|
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path scriptPath = m_toolboxItems[id].lua_script_path_string;
|
const std::filesystem::path scriptPath = std::filesystem::u8path(m_toolboxItems[id].lua_script_path_string);
|
||||||
m_luaRunner.run(id, scriptPath, getLuaTimeoutSecondsForId(id), [this, id]() {
|
const std::string toolPath = m_toolboxItems[id].path;
|
||||||
m_webview.saucer::webview::execute(std::format("enableButtonByToolId({})", id));
|
auto app = m_app;
|
||||||
m_webview.reload();
|
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) {
|
std::string ToolboxWindow::run_lua_path(const std::string& toolPath) {
|
||||||
m_luaRunner.cancel(id);
|
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() {
|
void ToolboxWindow::debug_print() {
|
||||||
std::cout << "ToolboxWindow::debug_print()" << std::endl;
|
std::cout << "ToolboxWindow::debug_print()" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolboxWindow::~ToolboxWindow() {
|
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;
|
m_runningMonitor = false;
|
||||||
if (m_threadMonitor.joinable()) {
|
if (m_threadMonitor.joinable()) {
|
||||||
m_threadMonitor.join();
|
m_threadMonitor.join();
|
||||||
@@ -242,11 +332,12 @@ ToolboxWindow::~ToolboxWindow() {
|
|||||||
|
|
||||||
void ToolboxWindow::monitor_focus() {
|
void ToolboxWindow::monitor_focus() {
|
||||||
while (m_runningMonitor) {
|
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();
|
const bool focused = m_webview.parent().focused();
|
||||||
if (focused) {
|
if (focused) {
|
||||||
m_seenFocusSinceShow = true;
|
m_seenFocusSinceShow.store(true, std::memory_order_release);
|
||||||
} else if (m_seenFocusSinceShow) {
|
} else if (m_seenFocusSinceShow.load(std::memory_order_acquire)) {
|
||||||
this->hide();
|
this->hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,24 +348,28 @@ void ToolboxWindow::monitor_focus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ToolboxWindow::open_file_system() {
|
void ToolboxWindow::open_file_system() {
|
||||||
if (const std::filesystem::path toolboxDir = userPath / ".Toolbox"; std::filesystem::exists(toolboxDir) && std::filesystem::is_directory(toolboxDir)) {
|
if (const std::filesystem::path toolboxDir = userPath / ".Toolbox";
|
||||||
std::string path = toolboxDir.string();
|
std::filesystem::exists(toolboxDir) && ToolRegistry::ensureToolboxDirectory(userPath)) {
|
||||||
std::string command;
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#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__
|
#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__
|
#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
|
#else
|
||||||
std::cerr << "Fehler: Plattform nicht unterstützt." << std::endl;
|
std::cerr << "Fehler: Plattform nicht unterstützt." << std::endl;
|
||||||
return;
|
return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (const int result = std::system(command.c_str()); result != 0) {
|
|
||||||
std::cerr << "Fehler beim Öffnen des Verzeichnisses: " << result << std::endl;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
|
std::cerr << "Fehler: Das Verzeichnis .Toolbox existiert nicht." << std::endl;
|
||||||
}
|
}
|
||||||
@@ -301,34 +396,107 @@ void ToolboxWindow::open_cmd() {
|
|||||||
#endif
|
#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:
|
int ToolboxWindow::getWindowOpacity() const {
|
||||||
void ToolboxWindow::openEinst() {
|
return m_lastOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
optionWindow& ToolboxWindow::optionWindowInstance() {
|
||||||
if (!m_optionWindow) {
|
if (!m_optionWindow) {
|
||||||
m_optionWindow = std::make_unique<optionWindow>(
|
m_optionWindow = std::make_unique<optionWindow>(
|
||||||
m_app,
|
m_app,
|
||||||
[this]() { return getLuaTimeoutSeconds(); },
|
[this]() { return getActiveToolDetails(); },
|
||||||
[this](int seconds) { setLuaTimeoutSeconds(seconds); },
|
[this](const std::string& payload) { return saveActiveToolDetails(payload); },
|
||||||
[this]() { return getActiveToolName(); },
|
[this]() { return openActiveToolFolder(); },
|
||||||
[this]() { return getToolConfigIni(); },
|
[this](const std::string &payload) { return createTool(payload); },
|
||||||
[this](const std::string &content) { setToolConfigIni(content); },
|
[this]() { openLuaHelp(); },
|
||||||
[this]() { openIndex(); }
|
[this]() { openIndex(); }
|
||||||
);
|
);
|
||||||
m_optionWindow->initialize();
|
m_optionWindow->initialize();
|
||||||
}
|
}
|
||||||
m_optionWindow->show();
|
return *m_optionWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToolboxWindow::openEinst(int id) {
|
void ToolboxWindow::openEinst() {
|
||||||
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
|
optionWindowInstance().show();
|
||||||
std::cerr << "openEinst: invalid tool id " << id << std::endl;
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
m_activeToolId = id;
|
m_activeToolPath = candidate;
|
||||||
ensureConfigExists(id);
|
|
||||||
openEinst();
|
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() {
|
void ToolboxWindow::centerWindow() {
|
||||||
WindowPlacement::center(m_hwnd);
|
WindowPlacement::center(m_hwnd);
|
||||||
}
|
}
|
||||||
@@ -344,7 +512,7 @@ void ToolboxWindow::resizeWindow(double widthPercent, double heightPercent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ToolboxWindow::openIndex() {
|
void ToolboxWindow::openIndex() {
|
||||||
if (!m_bShow) {
|
if (!m_isVisible.load(std::memory_order_acquire)) {
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
if (!serveHtmlFile("html/index.html", false)) {
|
if (!serveHtmlFile("html/index.html", false)) {
|
||||||
@@ -359,7 +527,7 @@ void ToolboxWindow::getNewContent()
|
|||||||
fillToolboxMenu();
|
fillToolboxMenu();
|
||||||
SetJsonItems();
|
SetJsonItems();
|
||||||
m_webview.saucer::webview::execute("reloadContent()");
|
m_webview.saucer::webview::execute("reloadContent()");
|
||||||
isIndex = true;
|
m_isIndex.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToolboxWindow::toggleDevTools() {
|
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())) {
|
if (id < 0 || id >= static_cast<int>(m_toolboxItems.size())) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return std::filesystem::path(m_toolboxItems[id].path) / "config.ini";
|
return std::filesystem::u8path(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ToolboxWindow::getLuaTimeoutSecondsForId(int id) const {
|
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);
|
return m_configStore.getInt(configPath, "lua_timeout_seconds", 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ToolboxWindow::getLuaTimeoutSeconds() {
|
std::string ToolboxWindow::getActiveToolDetails() const {
|
||||||
if (m_activeToolId < 0) {
|
const auto result = ToolRegistry::getToolDetails(userDirectory(), m_activeToolPath);
|
||||||
return 30;
|
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) {
|
std::string ToolboxWindow::saveActiveToolDetails(const std::string& payload) {
|
||||||
if (m_activeToolId < 0) {
|
nlohmann::json response;
|
||||||
return;
|
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);
|
return response.dump();
|
||||||
if (configPath.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_configStore.setInt(configPath, "lua_timeout_seconds", seconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ToolboxWindow::getToolConfigIni() const {
|
std::string ToolboxWindow::openActiveToolFolder() const {
|
||||||
if (m_activeToolId < 0) {
|
if (!ToolRegistry::getToolDetails(userDirectory(), m_activeToolPath).success) {
|
||||||
return {};
|
return nlohmann::json({{"success", false}, {"message", "Der Toolordner wurde nicht gefunden."}}).dump();
|
||||||
}
|
}
|
||||||
const auto configPath = getToolConfigPath(m_activeToolId);
|
#ifdef _WIN32
|
||||||
if (configPath.empty()) {
|
const auto result = reinterpret_cast<std::intptr_t>(
|
||||||
return {};
|
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);
|
#endif
|
||||||
}
|
return nlohmann::json({{"success", true}, {"message", "Toolordner wurde geoeffnet."}}).dump();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+67
-25
@@ -6,6 +6,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
@@ -65,7 +66,10 @@ struct TrayController::Impl {
|
|||||||
}} {}
|
}} {}
|
||||||
|
|
||||||
bool initialize() {
|
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);
|
const HICON icon = toolbox_load_small_icon(GetModuleHandleW(nullptr), IDI_ICON1);
|
||||||
trayIcon = std::make_unique<toolboxTray>("toolbox.ico", "Toolbox");
|
trayIcon = std::make_unique<toolboxTray>("toolbox.ico", "Toolbox");
|
||||||
@@ -84,26 +88,33 @@ struct TrayController::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
|
stopping.store(true);
|
||||||
uninstallExplorerHook();
|
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();
|
trayIcon.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Impl* current() {
|
|
||||||
return activeController.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void toggleToolboxWindow(tray_menu*) {
|
static void toggleToolboxWindow(tray_menu*) {
|
||||||
Impl* controller = current();
|
std::lock_guard<std::mutex> lock(activeControllerMutex);
|
||||||
if (!controller) {
|
if (!activeController || activeController->stopping.load()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolboxWindow& window = controller->window;
|
ToolboxWindow& window = activeController->window;
|
||||||
if (window.m_bShow) {
|
if (window.isVisible()) {
|
||||||
window.hide();
|
window.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,6 +123,10 @@ struct TrayController::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool reinitTray() {
|
bool reinitTray() {
|
||||||
|
std::lock_guard<std::mutex> lock(trayMutex);
|
||||||
|
if (stopping.load()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!trayIcon || state.reinitScheduled) {
|
if (!trayIcon || state.reinitScheduled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -143,29 +158,50 @@ struct TrayController::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void retryTrayReinitAsync() {
|
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);
|
Sleep(500);
|
||||||
|
|
||||||
|
bool recovered = false;
|
||||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||||
Impl* controller = current();
|
if (stopToken.stop_requested() || stopping.load()) {
|
||||||
if (controller && isExplorerReady() && controller->reinitTray()) {
|
break;
|
||||||
return;
|
}
|
||||||
|
if (isExplorerReady() && reinitTray()) {
|
||||||
|
recovered = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sleep(500 * (attempt + 1));
|
Sleep(500 * (attempt + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr, "Tray konnte nach Explorer-Neustart nicht wiederhergestellt werden\n");
|
if (!recovered && !stopToken.stop_requested() && !stopping.load()) {
|
||||||
}).detach();
|
std::fprintf(stderr, "Tray konnte nach Explorer-Neustart nicht wiederhergestellt werden\n");
|
||||||
|
}
|
||||||
|
retryRunning.store(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void scheduleTrayReinit() {
|
void scheduleTrayReinit() {
|
||||||
const DWORD now = GetTickCount();
|
if (stopping.load()) {
|
||||||
if (now - state.lastExplorerRestartTime < minExplorerRestartDelayMs) {
|
|
||||||
return;
|
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);
|
Sleep(300);
|
||||||
|
|
||||||
if (!reinitTray()) {
|
if (!reinitTray()) {
|
||||||
@@ -175,8 +211,8 @@ struct TrayController::Impl {
|
|||||||
|
|
||||||
static void CALLBACK handleExplorerEvent(HWINEVENTHOOK, DWORD event, HWND hwnd, LONG idObject, LONG, DWORD,
|
static void CALLBACK handleExplorerEvent(HWINEVENTHOOK, DWORD event, HWND hwnd, LONG idObject, LONG, DWORD,
|
||||||
DWORD) {
|
DWORD) {
|
||||||
Impl* controller = current();
|
std::lock_guard<std::mutex> lock(activeControllerMutex);
|
||||||
if (!controller || idObject != OBJID_WINDOW || !isTaskbarWindow(hwnd)) {
|
if (!activeController || activeController->stopping.load() || idObject != OBJID_WINDOW || !isTaskbarWindow(hwnd)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +221,7 @@ struct TrayController::Impl {
|
|||||||
std::printf(isExplorerReady()
|
std::printf(isExplorerReady()
|
||||||
? "Explorer ist bereit - Tray wird neu initialisiert\n"
|
? "Explorer ist bereit - Tray wird neu initialisiert\n"
|
||||||
: "Explorer noch nicht bereit - Warten...\n");
|
: "Explorer noch nicht bereit - Warten...\n");
|
||||||
controller->scheduleTrayReinit();
|
activeController->scheduleTrayReinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,10 +254,16 @@ struct TrayController::Impl {
|
|||||||
std::array<tray_menu, 6> menuItems;
|
std::array<tray_menu, 6> menuItems;
|
||||||
TrayState state;
|
TrayState state;
|
||||||
HWINEVENTHOOK explorerHook = nullptr;
|
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)
|
TrayController::TrayController(ToolboxWindow& window)
|
||||||
: m_impl(std::make_unique<Impl>(window)) {}
|
: m_impl(std::make_unique<Impl>(window)) {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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