+ {% assign prev_url = include.prev_url | default: page.prev_url %}
+ {% assign prev_label = include.prev_label| default: page.prev_label | default: 'Home' %}
+ {% assign next_url = include.next_url | default: page.next_url %}
+ {% assign next_label = include.next_label| default: page.next_label | default: 'Next' %}
+
+ {% if prev_url %}
+
← {{ prev_label }}
+ {% else %}
+
+ {% endif %}
+
+ {% if next_url %}
+
{{ next_label }} →
+ {% endif %}
+
diff --git a/docs/applications.md b/docs/applications.md
new file mode 100644
index 0000000..c1957ab
--- /dev/null
+++ b/docs/applications.md
@@ -0,0 +1,21 @@
+---
+title: Applications
+nav_order: 4
+prev_url: /updates.html
+prev_label: Updates
+next_url: /winget.html
+next_label: Install Winget Applications
+parent: UI Overview
+has_toc: false
+---
+# Applications
+
+
+
+Applications can be installed in three different ways:
+
+* Winget (using an AppList.json file)
+* Bring Your Own Applications (using files you provide - can also be used to run command lines with or without content)
+* Apps Script Variables (key/value pairs used in conjunction with a PowerShell script to install custom applications)
+
+{% include page_nav.html %}
diff --git a/docs/appsscriptvariables.md b/docs/appsscriptvariables.md
new file mode 100644
index 0000000..cc72681
--- /dev/null
+++ b/docs/appsscriptvariables.md
@@ -0,0 +1,138 @@
+---
+title: Apps Script Variables
+nav_order: 7
+prev_url: /byoapps.html
+prev_label: BYO Applications
+next_url: /M365appsoffice.html
+next_label: M365 Apps Office
+parent: Applications
+grand_parent: UI Overview
+---
+# Apps Script Variables
+
+
+
+Apps Script Variables are key value pairs that are used to create a hashtable that is passed to the `BuildFFUVM.ps1` script (stored in the `$AppScriptVariables` parameter as a hashtable). At build time, `BuildFFUVM.ps1` will export the `$AppsScriptVariables` hashtable to an `AppsScriptVariables.json` file in the `$OrchestrationPath` folder (`$AppsPath\Orchestration`). You can also manually create your own `AppsScriptVariables.json `file and place it in the `$AppsPath\Orchestration` folder.
+
+In the VM, the `Orchestrator.ps1` file will call `Invoke-AppsScript.ps1` if `AppsScriptVariables.json` exists. `Invoke-AppsScript.ps1` must be modified to handle your variables.
+
+`Invoke-AppsScript.ps1` has the following commented example of how to modify the file:
+
+```
+# Example of how to use the AppsScriptVariables hashtable to control script execution
+
+# Note: The UI saves the values as strings, so if you type true for a value, it'll save to the config file as a string, not boolean
+
+# Example: Check if a variable named 'foo' is set to string 'bar' and run a script accordingly
+# if ($AppsScriptVariables['foo'] -eq 'bar') {
+# Write-Host "Foo would have installed"
+# }
+# else {
+# Write-Host "Foo would not have installed"
+# }
+
+# Example: Check if a variable named 'Teams' is set to string 'true' and run a script accordingly
+# if ($AppsScriptVariables['Teams'] -eq 'true') {
+# Write-Host "Teams would have been installed"
+# }
+# else {
+# Write-Host "Teams would not have been installed"
+# }
+```
+
+## Why use Apps Script Variables?
+
+This allows for you to create a dynamic task sequence via a PowerShell script with simple if statements to run apps, commands, etc. This is all driven by your `FFUConfig.json` file. For example, the following `FFUConfig.json` file contains an AppsScriptVariables hashtable of foo and vmwaretools like the screenshot above. If you build servers that require vmware tools, you may set the value to true. However there may be situations where you don't need vmwaretools installed. If that's the case, you set vmwaretools to false. This allows for your `Invoke-AppsScript.ps1` file to stay the same and all you have to do is adjust the variables.
+
+```
+{
+ "AdditionalFFUFiles": [],
+ "AllowExternalHardDiskMedia": false,
+ "AllowVHDXCaching": false,
+ "AppListPath": "C:\\FFUDevelopment\\Apps\\AppList.json",
+ "AppsPath": "C:\\FFUDevelopment\\Apps",
+ "AppsScriptVariables": {
+ "foo": "bar",
+ "vmwaretools": "true"
+ },
+ "BuildUSBDrive": false,
+ "CleanupAppsISO": true,
+ "CleanupCaptureISO": true,
+ "CleanupDeployISO": true,
+ "CleanupDrivers": false,
+ "CompactOS": true,
+ "CompressDownloadedDriversToWim": false,
+ "CopyAdditionalFFUFiles": false,
+ "CopyAutopilot": false,
+ "CopyDrivers": false,
+ "CopyOfficeConfigXML": false,
+ "CopyPEDrivers": false,
+ "CopyPPKG": false,
+ "CopyUnattend": false,
+ "CreateCaptureMedia": true,
+ "CreateDeploymentMedia": true,
+ "CustomFFUNameTemplate": "{WindowsRelease}_{WindowsVersion}_{SKU}_{yyyy}-{MM}-{dd}_{HH}{mm}",
+ "Disksize": 53687091200,
+ "DownloadDrivers": false,
+ "DriversFolder": "C:\\FFUDevelopment\\Drivers",
+ "DriversJsonPath": "C:\\FFUDevelopment\\Drivers\\Drivers.json",
+ "FFUCaptureLocation": "C:\\FFUDevelopment\\FFU",
+ "FFUDevelopmentPath": "C:\\FFUDevelopment",
+ "FFUPrefix": "_FFU",
+ "InjectUnattend": false,
+ "InstallApps": true,
+ "InstallDrivers": false,
+ "InstallOffice": false,
+ "InstallWingetApps": false,
+ "ISOPath": "",
+ "LogicalSectorSizeBytes": 512,
+ "MaxUSBDrives": 5,
+ "MediaType": "Consumer",
+ "Memory": 4294967296,
+ "OfficeConfigXMLFile": "",
+ "OfficePath": "C:\\FFUDevelopment\\Apps\\Office",
+ "Optimize": true,
+ "OptionalFeatures": "",
+ "OrchestrationPath": "C:\\FFUDevelopment\\Apps\\Orchestration",
+ "PEDriversFolder": "C:\\FFUDevelopment\\PEDrivers",
+ "Processors": 4,
+ "ProductKey": "",
+ "PromptExternalHardDiskMedia": true,
+ "RemoveApps": false,
+ "RemoveFFU": false,
+ "RemoveUpdates": false,
+ "ShareName": "FFUCaptureShare",
+ "Threads": 5,
+ "UpdateADK": true,
+ "UpdateEdge": true,
+ "UpdateLatestCU": true,
+ "UpdateLatestDefender": true,
+ "UpdateLatestMicrocode": false,
+ "UpdateLatestMSRT": true,
+ "UpdateLatestNet": true,
+ "UpdateOneDrive": true,
+ "UpdatePreviewCU": false,
+ "USBDriveList": {},
+ "UseDriversAsPEDrivers": false,
+ "UserAppListPath": "C:\\FFUDevelopment\\Apps\\UserAppList.json",
+ "Username": "ffu_user",
+ "Verbose": false,
+ "VMHostIPAddress": "192.168.1.169",
+ "VMLocation": "C:\\FFUDevelopment\\VM",
+ "VMSwitchName": "External",
+ "WindowsArch": "x64",
+ "WindowsLang": "en-us",
+ "WindowsRelease": 11,
+ "WindowsSKU": "Pro",
+ "WindowsVersion": "25H2"
+}
+
+```
+
+Example command line to run with vmwaretools set to false and foo set to foo. This will create the `AppsScriptVariables.json` file in the Orchestration folder with the updated values of `foo=foo` and `vmwaretools=false` without the need to modify the config file.
+
+`.\BuildFFUVM.ps1 -configFile 'C:\FFUDevelopment\config\FFUConfig.json' -appsScriptVariables @{foo='foo'; vmwaretools='false'}`
+
+
+
+{% include page_nav.html %}
diff --git a/docs/assets/js/external-links.js b/docs/assets/js/external-links.js
new file mode 100644
index 0000000..09618d0
--- /dev/null
+++ b/docs/assets/js/external-links.js
@@ -0,0 +1,91 @@
+(function () {
+ 'use strict';
+
+ function HasToken(tokens, token) {
+ for (var i = 0; i < tokens.length; i++) {
+ if (tokens[i] === token) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function AddRelToken(anchor, token) {
+ var rel = (anchor.getAttribute('rel') || '').trim();
+ var tokens = rel ? rel.split(/\s+/) : [];
+
+ if (!HasToken(tokens, token)) {
+ tokens.push(token);
+ }
+
+ anchor.setAttribute('rel', tokens.join(' ').trim());
+ }
+
+ function IsExternalHttpLink(url) {
+ if (!url) {
+ return false;
+ }
+
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+ return false;
+ }
+
+ return url.origin !== window.location.origin;
+ }
+
+ function InitExternalLinksNewTab() {
+ var mainContent = document.querySelector('.main-content');
+ if (!mainContent) {
+ return;
+ }
+
+ var anchors = mainContent.querySelectorAll('a[href]');
+ for (var i = 0; i < anchors.length; i++) {
+ var anchor = anchors[i];
+ var href = (anchor.getAttribute('href') || '').trim();
+
+ if (!href) {
+ continue;
+ }
+
+ if (href.charAt(0) === '#') {
+ continue;
+ }
+
+ if (href.indexOf('mailto:') === 0 || href.indexOf('tel:') === 0 || href.indexOf('javascript:') === 0) {
+ continue;
+ }
+
+ var url = null;
+ try {
+ url = new URL(href, window.location.href);
+ } catch (e) {
+ continue;
+ }
+
+ if (!IsExternalHttpLink(url)) {
+ continue;
+ }
+
+ var target = (anchor.getAttribute('target') || '').trim();
+
+ if (!target) {
+ anchor.setAttribute('target', '_blank');
+ target = '_blank';
+ }
+
+ if (target === '_blank') {
+ AddRelToken(anchor, 'noopener');
+ AddRelToken(anchor, 'noreferrer');
+ }
+ }
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', InitExternalLinksNewTab);
+ return;
+ }
+
+ InitExternalLinksNewTab();
+})();
\ No newline at end of file
diff --git a/docs/assets/js/image-zoom.js b/docs/assets/js/image-zoom.js
new file mode 100644
index 0000000..44e54c8
--- /dev/null
+++ b/docs/assets/js/image-zoom.js
@@ -0,0 +1,22 @@
+(function () {
+ 'use strict';
+
+ function InitImageZoom() {
+ if (window.mediumZoom === undefined) {
+ return;
+ }
+
+ window.mediumZoom('.main-content img:not(.no-zoom):not([src$=".svg"])', {
+ margin: 24,
+ background: 'rgba(0,0,0,0.80)',
+ scrollOffset: 0
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', InitImageZoom);
+ return;
+ }
+
+ InitImageZoom();
+})();
\ No newline at end of file
diff --git a/docs/assets/js/page-toc.js b/docs/assets/js/page-toc.js
new file mode 100644
index 0000000..1fd6563
--- /dev/null
+++ b/docs/assets/js/page-toc.js
@@ -0,0 +1,403 @@
+(function () {
+ 'use strict';
+
+ var scrollSpyDispose = null;
+ var resizeReinitTimerId = null;
+ var inlineMaxVisibleItems = 4;
+
+ function IsRightTocEnabled() {
+ var meta = document.querySelector('meta[name="ffu-right-toc"]');
+ if (meta && meta.content && meta.content.toLowerCase() === 'false') {
+ return false;
+ }
+
+ return true;
+ }
+
+ function IsDesktopViewport() {
+ try {
+ return window.matchMedia && window.matchMedia('(min-width: 66.5rem)').matches;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function RemoveExistingToc() {
+ if (scrollSpyDispose) {
+ scrollSpyDispose();
+ scrollSpyDispose = null;
+ }
+
+ var existingTocs = document.querySelectorAll('.page-toc');
+ for (var i = 0; i < existingTocs.length; i++) {
+ existingTocs[i].remove();
+ }
+
+ var wrap = document.querySelector('.main-content-wrap');
+ if (wrap) {
+ wrap.classList.remove('has-page-toc');
+ }
+ }
+
+ function InsertInlineToc(main, toc) {
+ if (!main || !toc) {
+ return;
+ }
+
+ var title = main.querySelector('h1');
+ if (title && title.parentNode === main) {
+ if (title.nextSibling) {
+ main.insertBefore(toc, title.nextSibling);
+ return;
+ }
+
+ main.appendChild(toc);
+ return;
+ }
+
+ if (main.firstChild) {
+ main.insertBefore(toc, main.firstChild);
+ return;
+ }
+
+ main.appendChild(toc);
+ }
+
+ function GetHeadings(container) {
+ var headings = container.querySelectorAll('h2, h3');
+ var results = [];
+
+ for (var i = 0; i < headings.length; i++) {
+ var heading = headings[i];
+
+ if (heading.classList.contains('no_toc')) {
+ continue;
+ }
+
+ var id = heading.getAttribute('id');
+ if (!id) {
+ continue;
+ }
+
+ var text = (heading.textContent || '').trim();
+ if (!text) {
+ continue;
+ }
+
+ results.push({
+ level: heading.tagName.toLowerCase(),
+ id: id,
+ text: text
+ });
+ }
+
+ return results;
+ }
+
+ function BuildToc(headings, options) {
+ var variant = (options && options.variant) ? options.variant : 'right';
+ var maxVisible = (options && options.maxVisible) ? options.maxVisible : 0;
+ var isInline = 'inline' === variant;
+
+ var nav = document.createElement('nav');
+ nav.className = 'page-toc' + (isInline ? ' page-toc--inline' : '');
+ nav.setAttribute('aria-label', 'On this page');
+
+ var title = document.createElement('div');
+ title.className = 'page-toc__title';
+ title.textContent = 'In this article';
+ nav.appendChild(title);
+
+ var list = document.createElement('ul');
+ list.className = 'page-toc__list';
+ list.id = 'page-toc-list';
+
+ for (var i = 0; i < headings.length; i++) {
+ var item = headings[i];
+
+ var li = document.createElement('li');
+ li.className = 'page-toc__item page-toc__item--' + item.level;
+
+ var a = document.createElement('a');
+ a.className = 'page-toc__link';
+ a.href = '#' + item.id;
+ a.textContent = item.text;
+
+ li.appendChild(a);
+ list.appendChild(li);
+
+ if (isInline && maxVisible > 0 && i >= maxVisible) {
+ li.classList.add('is-hidden');
+ }
+ }
+
+ nav.appendChild(list);
+
+ if (isInline && maxVisible > 0 && headings.length > maxVisible) {
+ var hiddenCount = headings.length - maxVisible;
+ var isExpanded = false;
+
+ var toggle = document.createElement('button');
+ toggle.type = 'button';
+ toggle.className = 'page-toc__toggle';
+ toggle.setAttribute('aria-controls', list.id);
+ toggle.setAttribute('aria-expanded', 'false');
+
+ function SetToggleText() {
+ if (isExpanded) {
+ toggle.textContent = 'Show less';
+ } else {
+ toggle.textContent = 'Show ' + hiddenCount + ' more';
+ }
+ }
+
+ function SetHiddenState() {
+ var items = list.querySelectorAll('.page-toc__item');
+ for (var j = 0; j < items.length; j++) {
+ if (j >= maxVisible) {
+ if (isExpanded) {
+ items[j].classList.remove('is-hidden');
+ } else {
+ items[j].classList.add('is-hidden');
+ }
+ }
+ }
+
+ toggle.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
+ SetToggleText();
+ }
+
+ toggle.addEventListener('click', function () {
+ isExpanded = !isExpanded;
+ SetHiddenState();
+ });
+
+ SetHiddenState();
+ nav.appendChild(toggle);
+ }
+
+ return nav;
+ }
+
+ function SetActiveTocLink(toc, activeId, keepVisibleInPanel) {
+ if (!toc) {
+ return;
+ }
+
+ var links = toc.querySelectorAll('.page-toc__link');
+ for (var i = 0; i < links.length; i++) {
+ var link = links[i];
+ var href = link.getAttribute('href') || '';
+ var isActive = ('#' + activeId) === href;
+
+ if (isActive) {
+ link.classList.add('is-active');
+
+ if (keepVisibleInPanel) {
+ /* Keep the active item visible inside the TOC panel (desktop/right TOC only) */
+ try {
+ link.scrollIntoView({ block: 'nearest' });
+ } catch (e) {
+ link.scrollIntoView();
+ }
+ }
+ } else {
+ link.classList.remove('is-active');
+ }
+ }
+ }
+
+ function SetupScrollSpy(main, toc, headings) {
+ if (!main || !toc || !headings || headings.length < 1) {
+ return null;
+ }
+
+ /* Scrollspy is desktop-only */
+ if (!IsDesktopViewport()) {
+ return null;
+ }
+
+ var headingElements = [];
+ for (var i = 0; i < headings.length; i++) {
+ var el = document.getElementById(headings[i].id);
+ if (el) {
+ headingElements.push(el);
+ }
+ }
+
+ if (headingElements.length < 1) {
+ return null;
+ }
+
+ var activeId = null;
+ var ticking = false;
+ var lockActiveUntilMs = 0;
+
+ function IsNearBottomOfPage() {
+ var thresholdPx = 24;
+ var scrollY = window.scrollY || window.pageYOffset || 0;
+ var viewportBottom = scrollY + window.innerHeight;
+ var pageHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
+
+ return viewportBottom >= (pageHeight - thresholdPx);
+ }
+
+ function GetCurrentHeadingId() {
+ /* If we're at the bottom, force the last heading active */
+ if (IsNearBottomOfPage()) {
+ return headingElements[headingElements.length - 1].getAttribute('id');
+ }
+
+ /* Choose the heading closest to the top "activation line" */
+ var activationLine = 16;
+ var current = null;
+
+ for (var i = 0; i < headingElements.length; i++) {
+ var rectTop = headingElements[i].getBoundingClientRect().top;
+
+ if (rectTop <= activationLine) {
+ current = headingElements[i];
+ continue;
+ }
+
+ if (null === current) {
+ current = headingElements[i];
+ }
+
+ break;
+ }
+
+ if (null === current) {
+ current = headingElements[0];
+ }
+
+ return current.getAttribute('id');
+ }
+
+ function Update() {
+ ticking = false;
+
+ /* If the viewport becomes narrow after load, avoid scroll fighting */
+ if (!IsDesktopViewport()) {
+ return;
+ }
+
+ if (Date.now() < lockActiveUntilMs) {
+ return;
+ }
+
+ var currentId = GetCurrentHeadingId();
+ if (!currentId || currentId === activeId) {
+ return;
+ }
+
+ activeId = currentId;
+ SetActiveTocLink(toc, activeId, true);
+ }
+
+ function OnScrollOrResize() {
+ if (ticking) {
+ return;
+ }
+
+ ticking = true;
+ window.requestAnimationFrame(Update);
+ }
+
+ function OnTocClick(evt) {
+ var target = evt.target;
+ if (!target || !target.classList || !target.classList.contains('page-toc__link')) {
+ return;
+ }
+
+ var href = target.getAttribute('href') || '';
+ if (href.charAt(0) !== '#') {
+ return;
+ }
+
+ var id = href.substring(1);
+ if (!id) {
+ return;
+ }
+
+ /* Prevent scrollspy from immediately overriding the clicked section */
+ lockActiveUntilMs = Date.now() + 800;
+
+ activeId = id;
+ SetActiveTocLink(toc, activeId, true);
+ }
+
+ window.addEventListener('scroll', OnScrollOrResize, { passive: true });
+ window.addEventListener('resize', OnScrollOrResize);
+ toc.addEventListener('click', OnTocClick);
+
+ Update();
+
+ return function DisposeScrollSpy() {
+ window.removeEventListener('scroll', OnScrollOrResize);
+ window.removeEventListener('resize', OnScrollOrResize);
+ toc.removeEventListener('click', OnTocClick);
+ };
+ }
+
+ function InitRightToc() {
+ if (!IsRightTocEnabled()) {
+ RemoveExistingToc();
+ return;
+ }
+
+ var main = document.querySelector('.main-content main');
+ if (!main) {
+ return;
+ }
+
+ var headings = GetHeadings(main);
+ if (headings.length < 2) {
+ RemoveExistingToc();
+ return;
+ }
+
+ if (IsDesktopViewport()) {
+ RemoveExistingToc();
+
+ var wrap = document.querySelector('.main-content-wrap');
+ if (!wrap) {
+ return;
+ }
+
+ wrap.classList.add('has-page-toc');
+
+ var toc = BuildToc(headings, { variant: 'right' });
+ wrap.appendChild(toc);
+
+ scrollSpyDispose = SetupScrollSpy(main, toc, headings);
+ return;
+ }
+
+ /* Narrow viewports: place TOC at the top of the article (Learn-like) */
+ RemoveExistingToc();
+
+ var inlineToc = BuildToc(headings, { variant: 'inline', maxVisible: inlineMaxVisibleItems });
+ InsertInlineToc(main, inlineToc);
+ }
+
+ function OnViewportResize() {
+ if (null !== resizeReinitTimerId) {
+ window.clearTimeout(resizeReinitTimerId);
+ }
+
+ resizeReinitTimerId = window.setTimeout(InitRightToc, 150);
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function () {
+ InitRightToc();
+ window.addEventListener('resize', OnViewportResize);
+ });
+
+ return;
+ }
+
+ InitRightToc();
+ window.addEventListener('resize', OnViewportResize);
+})();
\ No newline at end of file
diff --git a/docs/assets/js/vendor/medium-zoom.min.js b/docs/assets/js/vendor/medium-zoom.min.js
new file mode 100644
index 0000000..dbe319a
--- /dev/null
+++ b/docs/assets/js/vendor/medium-zoom.min.js
@@ -0,0 +1,2 @@
+/*! medium-zoom 1.1.0 | MIT License | https://github.com/francoischalifour/medium-zoom */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).mediumZoom=t()}(this,(function(){"use strict";var e=Object.assign||function(e){for(var t=1;t