diff --git a/.github/template/README.md b/.github/template/README.md index c7b5b7e4..fcda5aea 100644 --- a/.github/template/README.md +++ b/.github/template/README.md @@ -7,6 +7,14 @@ ## About %%myproject%% %%description%% +## WebAssembly Demo + +Try the live WebAssembly demo: +- Main: [https://%%myorg%%.github.io/%%myproject%%/](https://%%myorg%%.github.io/%%myproject%%/) +- Develop: [https://%%myorg%%.github.io/%%myproject%%/develop/](https://%%myorg%%.github.io/%%myproject%%/develop/) + +The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. + ## More Details diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 00000000..83aa49c3 --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,59 @@ +name: Build Intro WASM and Deploy to GitHub Pages + +on: + pull_request: + release: + types: [published] + push: + branches: [main, develop] + tags: ['**'] + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 'latest' + + - name: Install Ninja + run: sudo apt-get install -y ninja-build + + - name: Configure CMake + run: emcmake cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build all WASM targets + run: emmake cmake --build build --target web-dist + + - name: Prepare deployment + run: | + # web-dist target already created build/web-dist/ + # Just copy it to dist/ for GitHub Pages action + cp -r build/web-dist dist + + - name: Determine deploy path + id: deploy-path + if: github.event_name != 'pull_request' && github.event_name != 'release' + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + echo "path=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/main ]]; then + echo "path=." >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" == refs/heads/develop ]]; then + echo "path=develop" >> $GITHUB_OUTPUT + fi + + - name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' && github.event_name != 'release' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + destination_dir: ${{ steps.deploy-path.outputs.path }} + keep_files: true diff --git a/CMakeLists.txt b/CMakeLists.txt index a5ac5435..95d0a27f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ project( LANGUAGES CXX C) include(cmake/PreventInSourceBuilds.cmake) +include(cmake/Emscripten.cmake) include(ProjectOptions.cmake) @@ -60,6 +61,11 @@ add_subdirectory(configured_files) # Adding the src: add_subdirectory(src) +# Create unified web deployment directory (for WASM builds) +if(EMSCRIPTEN) + myproject_create_web_dist() +endif() + # Don't even look at tests if we're not top level if(NOT PROJECT_IS_TOP_LEVEL) return() diff --git a/ProjectOptions.cmake b/ProjectOptions.cmake index 3d260944..fcd472bd 100644 --- a/ProjectOptions.cmake +++ b/ProjectOptions.cmake @@ -8,7 +8,11 @@ include(CheckCXXSourceCompiles) macro(myproject_supports_sanitizers) - if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) + # Emscripten doesn't support sanitizers + if(EMSCRIPTEN) + set(SUPPORTS_UBSAN OFF) + set(SUPPORTS_ASAN OFF) + elseif((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang.*" OR CMAKE_CXX_COMPILER_ID MATCHES ".*GNU.*") AND NOT WIN32) message(STATUS "Sanity checking UndefinedBehaviorSanitizer, it should be supported on this platform") set(TEST_PROGRAM "int main() { return 0; }") @@ -166,19 +170,22 @@ macro(myproject_local_options) "" "") - if(myproject_ENABLE_USER_LINKER) - include(cmake/Linker.cmake) - myproject_configure_linker(myproject_options) - endif() + # Linker and sanitizers not supported in Emscripten + if(NOT EMSCRIPTEN) + if(myproject_ENABLE_USER_LINKER) + include(cmake/Linker.cmake) + myproject_configure_linker(myproject_options) + endif() - include(cmake/Sanitizers.cmake) - myproject_enable_sanitizers( - myproject_options - ${myproject_ENABLE_SANITIZER_ADDRESS} - ${myproject_ENABLE_SANITIZER_LEAK} - ${myproject_ENABLE_SANITIZER_UNDEFINED} - ${myproject_ENABLE_SANITIZER_THREAD} - ${myproject_ENABLE_SANITIZER_MEMORY}) + include(cmake/Sanitizers.cmake) + myproject_enable_sanitizers( + myproject_options + ${myproject_ENABLE_SANITIZER_ADDRESS} + ${myproject_ENABLE_SANITIZER_LEAK} + ${myproject_ENABLE_SANITIZER_UNDEFINED} + ${myproject_ENABLE_SANITIZER_THREAD} + ${myproject_ENABLE_SANITIZER_MEMORY}) + endif() set_target_properties(myproject_options PROPERTIES UNITY_BUILD ${myproject_ENABLE_UNITY_BUILD}) diff --git a/README.md b/README.md index a860cb0b..eeedbbb8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ It includes * a basic CLI example * examples for fuzz, unit, and constexpr testing * large GitHub action testing matrix + * WebAssembly build support with automatic GitHub Pages deployment + +**Live Demo:** If you enable GitHub Pages in your project created from this template, you'll have a working example like this: +- Main: [https://cpp-best-practices.github.io/cmake_template/](https://cpp-best-practices.github.io/cmake_template/) +- Develop: [https://cpp-best-practices.github.io/cmake_template/develop/](https://cpp-best-practices.github.io/cmake_template/develop/) + +The `main` branch deploys to the root, `develop` to `/develop/`, and tags to `/tagname/`. It requires diff --git a/cmake/Emscripten.cmake b/cmake/Emscripten.cmake new file mode 100644 index 00000000..e6115ab8 --- /dev/null +++ b/cmake/Emscripten.cmake @@ -0,0 +1,242 @@ +# cmake/Emscripten.cmake +# Emscripten/WebAssembly build configuration + +# Common paths for web assets +set(myproject_WEB_DIR "${CMAKE_SOURCE_DIR}/web") +set(myproject_COI_WORKER "${myproject_WEB_DIR}/coi-serviceworker.min.js") +set(myproject_SHELL_TEMPLATE "${myproject_WEB_DIR}/shell_template.html.in") +set(myproject_INDEX_TEMPLATE "${myproject_WEB_DIR}/index_template.html.in") + +# Helper function to escape HTML special characters +function(escape_html output_var input) + set(result "${input}") + string(REPLACE "&" "&" result "${result}") + string(REPLACE "<" "<" result "${result}") + string(REPLACE ">" ">" result "${result}") + string(REPLACE "\"" """ result "${result}") + set(${output_var} "${result}" PARENT_SCOPE) +endfunction() + +# Detect if we're building with Emscripten +if(EMSCRIPTEN) + message(STATUS "Emscripten build detected - configuring for WebAssembly") + + # Set WASM build flag + set(myproject_WASM_BUILD ON CACHE BOOL "Building for WebAssembly" FORCE) + + # Sanitizers don't work with Emscripten + foreach(sanitizer ADDRESS LEAK UNDEFINED THREAD MEMORY) + set(myproject_ENABLE_SANITIZER_${sanitizer} OFF CACHE BOOL "Not supported with Emscripten") + endforeach() + + # Disable static analysis and strict warnings for Emscripten builds + foreach(option CLANG_TIDY CPPCHECK WARNINGS_AS_ERRORS) + set(myproject_ENABLE_${option} OFF CACHE BOOL "Disabled for Emscripten") + endforeach() + + # Disable testing - no way to execute WASM test targets + set(BUILD_TESTING OFF CACHE BOOL "No test runner for WASM") + + # WASM runtime configuration - tunable performance parameters + set(myproject_WASM_INITIAL_MEMORY "33554432" CACHE STRING + "Initial WASM memory in bytes (default: 32MB)") + set(myproject_WASM_PTHREAD_POOL_SIZE "4" CACHE STRING + "Pthread pool size for WASM builds (default: 4)") + set(myproject_WASM_ASYNCIFY_STACK_SIZE "65536" CACHE STRING + "Asyncify stack size in bytes (default: 64KB)") + + # For Emscripten WASM builds, FTXUI requires pthreads and native exception handling + # Set these flags early so they propagate to all dependencies + add_compile_options(-pthread -fwasm-exceptions) + add_link_options(-pthread -fwasm-exceptions) +endif() + +# Function to apply WASM settings to a target +function(myproject_configure_wasm_target target) + if(EMSCRIPTEN) + # Parse optional named arguments + set(options "") + set(oneValueArgs TITLE DESCRIPTION RESOURCES_DIR) + set(multiValueArgs "") + cmake_parse_arguments(WASM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Set defaults if not provided + if(NOT WASM_TITLE) + set(WASM_TITLE "${target}") + endif() + + if(NOT WASM_DESCRIPTION) + set(WASM_DESCRIPTION "WebAssembly application") + endif() + + # Register this target in the global WASM targets list + set_property(GLOBAL APPEND PROPERTY myproject_WASM_TARGETS "${target}") + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE "${WASM_TITLE}") + set_property(GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION "${WASM_DESCRIPTION}") + + target_compile_definitions(${target} PRIVATE myproject_WASM_BUILD=1) + + # Emscripten link flags + target_link_options(${target} PRIVATE + # Enable pthreads - REQUIRED by FTXUI's WASM implementation + "-sUSE_PTHREADS=1" + "-sPROXY_TO_PTHREAD=1" + "-sPTHREAD_POOL_SIZE=${myproject_WASM_PTHREAD_POOL_SIZE}" + # Enable asyncify for emscripten_sleep and async operations + "-sASYNCIFY=1" + "-sASYNCIFY_STACK_SIZE=${myproject_WASM_ASYNCIFY_STACK_SIZE}" + # Memory configuration + "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=${myproject_WASM_INITIAL_MEMORY}" + # Environment - need both web and worker for pthread support + "-sENVIRONMENT=web,worker" + # Export runtime methods for JavaScript interop + "-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']" + # Export malloc/free for MAIN_THREAD_EM_ASM usage + "-sEXPORTED_FUNCTIONS=['_main','_malloc','_free']" + # Debug: enable assertions for better error messages + "-sASSERTIONS=1" + ) + + # Embed resources into WASM binary (optional, per-target) + if(WASM_RESOURCES_DIR AND EXISTS "${WASM_RESOURCES_DIR}") + # Convert to absolute path to avoid issues with Emscripten path resolution + get_filename_component(ABS_RESOURCES_DIR "${WASM_RESOURCES_DIR}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") + + target_link_options(${target} PRIVATE + "--embed-file=${ABS_RESOURCES_DIR}@/resources" + ) + message(STATUS "Embedding resources for ${target} from ${ABS_RESOURCES_DIR}") + endif() + + # Configure the shell HTML template for this target + set(TARGET_NAME "${target}") + set(TARGET_TITLE "${WASM_TITLE}") + set(TARGET_DESCRIPTION "${WASM_DESCRIPTION}") + set(AT "@") # For escaping @ in npm package URLs + set(CONFIGURED_SHELL "${CMAKE_BINARY_DIR}/web/${target}_shell.html") + + # Generate target-specific shell file (configure_file creates parent directories automatically) + if(EXISTS "${myproject_SHELL_TEMPLATE}") + configure_file( + "${myproject_SHELL_TEMPLATE}" + "${CONFIGURED_SHELL}" + @ONLY + ) + + # Use the generated shell file + target_link_options(${target} PRIVATE + "--shell-file=${CONFIGURED_SHELL}" + ) + + # Add both template and configured file as link dependencies + set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS + "${myproject_SHELL_TEMPLATE}" + "${CONFIGURED_SHELL}" + ) + + message(STATUS "Configured WASM shell for ${target}: ${CONFIGURED_SHELL}") + else() + message(FATAL_ERROR "Shell template not found: ${myproject_SHELL_TEMPLATE}") + endif() + + # Copy service worker to target build directory for standalone target builds + if(EXISTS "${myproject_COI_WORKER}") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${myproject_COI_WORKER}" + "$/coi-serviceworker.min.js" + COMMENT "Copying coi-serviceworker.min.js to ${target} build directory" + ) + endif() + + # Set output suffix to .html + set_target_properties(${target} PROPERTIES SUFFIX ".html") + + message(STATUS "Configured ${target} for WebAssembly") + endif() +endfunction() + +# Create a unified web deployment directory with all WASM targets +function(myproject_create_web_dist) + if(NOT EMSCRIPTEN) + return() + endif() + + # Define output directory + set(WEB_DIST_DIR "${CMAKE_BINARY_DIR}/web-dist") + + # Get list of all WASM targets + get_property(WASM_TARGETS GLOBAL PROPERTY myproject_WASM_TARGETS) + + if(NOT WASM_TARGETS) + message(WARNING "No WASM targets registered. Skipping web-dist generation.") + return() + endif() + + # Generate HTML for app cards + set(WASM_APPS_HTML "") + foreach(target ${WASM_TARGETS}) + get_property(TITLE GLOBAL PROPERTY myproject_WASM_TARGET_${target}_TITLE) + get_property(DESCRIPTION GLOBAL PROPERTY myproject_WASM_TARGET_${target}_DESCRIPTION) + + # Escape HTML special characters to prevent injection + escape_html(TITLE_ESCAPED "${TITLE}") + escape_html(DESC_ESCAPED "${DESCRIPTION}") + + string(APPEND WASM_APPS_HTML +" +
${TITLE_ESCAPED}
+
${DESC_ESCAPED}
+
+") + endforeach() + + # Generate index.html from template + set(INDEX_OUTPUT "${WEB_DIST_DIR}/index.html") + + if(EXISTS "${myproject_INDEX_TEMPLATE}") + configure_file("${myproject_INDEX_TEMPLATE}" "${INDEX_OUTPUT}" @ONLY) + else() + message(WARNING "Index template not found: ${myproject_INDEX_TEMPLATE}") + endif() + + # Build list of copy commands + set(COPY_COMMANDS "") + + # For each WASM target, copy artifacts to subdirectory + # Each target gets its own service worker copy for standalone deployment + foreach(target ${WASM_TARGETS}) + get_target_property(TARGET_BINARY_DIR ${target} BINARY_DIR) + set(TARGET_DIST_DIR "${WEB_DIST_DIR}/${target}") + + # Copy WASM artifacts: .html (as index.html), .js, .wasm, and service worker + list(APPEND COPY_COMMANDS + COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIST_DIR}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${target}.html" + "${TARGET_DIST_DIR}/index.html" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${target}.js" + "${TARGET_DIST_DIR}/${target}.js" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${TARGET_BINARY_DIR}/${target}.wasm" + "${TARGET_DIST_DIR}/${target}.wasm" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${myproject_COI_WORKER}" + "${TARGET_DIST_DIR}/coi-serviceworker.min.js" + ) + endforeach() + + # Create custom target with all commands (part of ALL so it builds by default) + add_custom_target(web-dist ALL + COMMAND ${CMAKE_COMMAND} -E make_directory "${WEB_DIST_DIR}" + ${COPY_COMMANDS} + COMMENT "Creating unified web deployment directory" + ) + + # Ensure web-dist runs after all WASM targets are built + add_dependencies(web-dist ${WASM_TARGETS}) + + message(STATUS "Configured web-dist target with ${WASM_TARGETS}") +endfunction() diff --git a/cmake/Hardening.cmake b/cmake/Hardening.cmake index f77e1d21..905fc453 100644 --- a/cmake/Hardening.cmake +++ b/cmake/Hardening.cmake @@ -9,9 +9,9 @@ macro( message(STATUS "** Enabling Hardening (Target ${target}) **") if(MSVC) - set(NEW_COMPILE_OPTIONS "${NEW_COMPILE_OPTIONS} /sdl /DYNAMICBASE /guard:cf") + list(APPEND NEW_COMPILE_OPTIONS /sdl /DYNAMICBASE /guard:cf) message(STATUS "*** MSVC flags: /sdl /DYNAMICBASE /guard:cf /NXCOMPAT /CETCOMPAT") - set(NEW_LINK_OPTIONS "${NEW_LINK_OPTIONS} /NXCOMPAT /CETCOMPAT") + list(APPEND NEW_LINK_OPTIONS /NXCOMPAT /CETCOMPAT) elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang|GNU") list(APPEND NEW_CXX_DEFINITIONS -D_GLIBCXX_ASSERTIONS) @@ -89,9 +89,9 @@ macro( if(${global}) message(STATUS "** Setting hardening options globally for all dependencies") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_COMPILE_OPTIONS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${NEW_LINK_OPTIONS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${NEW_CXX_DEFINITIONS}") + add_compile_options(${NEW_COMPILE_OPTIONS}) + add_compile_definitions(${NEW_CXX_DEFINITIONS}) + add_link_options(${NEW_LINK_OPTIONS}) else() target_compile_options(${target} INTERFACE ${NEW_COMPILE_OPTIONS}) target_link_options(${target} INTERFACE ${NEW_LINK_OPTIONS}) diff --git a/configured_files/config.hpp.in b/configured_files/config.hpp.in index 86d5cb38..e8086136 100644 --- a/configured_files/config.hpp.in +++ b/configured_files/config.hpp.in @@ -1,5 +1,5 @@ -#ifndef MYPROJECT_CONFIG_HPP -#define MYPROJECT_CONFIG_HPP +#ifndef myproject_CONFIG_HPP +#define myproject_CONFIG_HPP // this is a basic example of how a CMake configured file might look // in this particular case, we are using it to set the version number of our executable diff --git a/src/ftxui_sample/CMakeLists.txt b/src/ftxui_sample/CMakeLists.txt index fc5c65e2..5ba9b564 100644 --- a/src/ftxui_sample/CMakeLists.txt +++ b/src/ftxui_sample/CMakeLists.txt @@ -17,3 +17,11 @@ target_link_system_libraries( ftxui::component) target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") + +# Apply WASM-specific configuration +if(EMSCRIPTEN) + myproject_configure_wasm_target(intro + TITLE "Intro - FTXUI Sample" + DESCRIPTION "FTXUI-based terminal application" + ) +endif() diff --git a/web/coi-serviceworker.min.js b/web/coi-serviceworker.min.js new file mode 100644 index 00000000..33fddc0f --- /dev/null +++ b/web/coi-serviceworker.min.js @@ -0,0 +1,2 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); diff --git a/web/index_template.html.in b/web/index_template.html.in new file mode 100644 index 00000000..a60f681e --- /dev/null +++ b/web/index_template.html.in @@ -0,0 +1,90 @@ + + + + + + @PROJECT_NAME@ - WebAssembly Applications + + + +
+

@PROJECT_NAME@

+

WebAssembly Applications

+ +
+ @WASM_APPS_HTML@ +
+ + +
+ + diff --git a/web/shell_template.html.in b/web/shell_template.html.in new file mode 100644 index 00000000..6b98449a --- /dev/null +++ b/web/shell_template.html.in @@ -0,0 +1,315 @@ + + + + + + @TARGET_TITLE@ + + + + + + + +
+

@TARGET_NAME@

+ +
+
+

Loading @TARGET_NAME@...

+

+
+ + + +
+ +
+

Command-Line Arguments via URL

+

Pass command-line arguments to the application using URL parameters:

+ +
+ Conversion Rules: +
    +
  • • Single-character params → short flags: ?v-v
  • +
  • • Multi-character params → long flags: ?version--version
  • +
  • • Params with values: ?file=test.txt--file test.txt
  • +
  • • Multiple params: ?v&help-v --help
  • +
+
+ +
+ Examples: +
    +
  • intro.html?version
  • +
  • intro.html?h
  • +
  • intro.html?file=data.txt
  • +
  • intro.html?verbose&config=settings.json
  • +
+
+
+
+ + + + + + {{{ SCRIPT }}} + +