// ISO_ScriptLauncherHotkeys.jsx
// Artist: https://www.jasonfletcher.info/
// Code generated by ChatGPT-5.5
// Date: June 10, 2026
// Description: This After Effects script will create a GUI so that the user can select a script and then execute it by clicking the "execute script" button or via hotkeys. 
// Hotkeys: The hotkeys (0-9) can be assigned by clicking on a script and then clicking on a hotkey button. Then just hit the hotkey on the keyboard to execute the script.
// QUIRK ABOUT HOTKEYS: After pressing a hotkey then you must click in the Scripts Listing area or Log area for a subsequent hotkey to function. This is due to a limitation with JSX scripts.
// Indexing: This script will auto index all JSX and JSXBIN scripts located within the same folder as this script (subfolders too) or user can select the root folder manually by clicking the "Change Dir" button. If the user manually changes the root folder, then the path is saved as a setting and used when the script is opened again in the future. If you install a script while After Effects is still open then you can click the "Refresh Dir" button.
// Installation Option 1: If you prefer for this script to be a dockable window, then install it within the <ScriptUI panels> folder. When you use the script you'll likely want to manually point it to the <Scripts> folder, which can be achieved by clicking the "Change Dir" button.
// Installation Option 2: If you prefer for this script to be a floating window, then install it within the <Scripts> folder. It will automatically index all available scripts from this location.

(function (thisObj) {

    app.beginUndoGroup("Script Launcher");

    try {

        function buildUI(thisObj) {

            var pal = (thisObj instanceof Panel)
                ? thisObj
                : new Window("palette", "Script Launcher", undefined, { resizeable: true });

            if (!pal) return null;

            pal.margins = 8;

            var scriptItems = [];
            var hotkeyMap = [];
            for (var h = 0; h < 10; h++) hotkeyMap[h] = null;

            var selectedItem = null;

            // =========================
            // PERSISTENT ROOT FOLDER
            // =========================
            var SETTINGS_SECTION = "ISO_ScriptLauncherHotkeys";
            var SETTINGS_KEY = "rootFolderPath";

            var rootFolder = File($.fileName).parent;

            // Load saved root folder if it exists
            if (app.settings.haveSetting(SETTINGS_SECTION, SETTINGS_KEY)) {
                var savedPath = app.settings.getSetting(SETTINGS_SECTION, SETTINGS_KEY);
                var testFolder = new Folder(savedPath);

                if (testFolder.exists) {
                    rootFolder = testFolder;
                }
            }

            var firstRun = true;

            // HOTKEY PANEL
            var hotkeyPanel = pal.add("panel", undefined, "Assign Hotkeys (0-9)");
            var hotkeyButtons = [];

            // Visual order: 1–9,0
            var visualOrder = [1,2,3,4,5,6,7,8,9,0];

            for (var v = 0; v < 10; v++) {

                var internalIndex = (v === 9) ? 0 : v + 1;

                hotkeyButtons[v] = hotkeyPanel.add("button", undefined, String(visualOrder[v]));
                hotkeyButtons[v].hotkeyIndex = internalIndex;
            }

            // SCRIPT LIST
            var listPanel = pal.add("panel", undefined, "Scripts Listing");
            var scriptList = listPanel.add("listbox", undefined, [], { multiselect: false });
            var buttonGroup = pal.add("group");
            var changeDirBtn = buttonGroup.add("button", undefined, "Change Dir");
            var refreshBtn = buttonGroup.add("button", undefined, "Refresh Dir");
            var executeBtn = buttonGroup.add("button", undefined, "Execute Script");

            // LOG
            var logPanel = pal.add("panel", undefined, "Log");
            var logBox = logPanel.add("edittext", undefined, "", {
                multiline: true,
                readonly: true,
                scrolling: true
            });

            function addLog(msg) {
                logBox.text = (logBox.text === "")
                    ? msg
                    : (logBox.text + "\r\n" + msg);
            }

            function sortAlpha(a, b) {
                a = a.toLowerCase();
                b = b.toLowerCase();
                return (a < b) ? -1 : (a > b) ? 1 : 0;
            }

            function hardDecode(str) {
                if (!str) return "";
                return str
                    .replace(/%20/g, " ")
                    .replace(/%23/g, "#")
                    .replace(/%5B/g, "[")
                    .replace(/%5D/g, "]")
                    .replace(/%28/g, "(")
                    .replace(/%29/g, ")")
                    .replace(/%2F/g, "/");
            }

            function getHotkeyIndexByFile(fsName) {
                for (var i = 0; i < 10; i++) {
                    if (hotkeyMap[i] === fsName) return i;
                }
                return -1;
            }

            function scanFolder(folder, rootPath, groups) {

                var files = folder.getFiles();
                var folderPath = hardDecode(folder.fsName);
                rootPath = hardDecode(rootPath);

                for (var i = 0; i < files.length; i++) {

                    var f = files[i];

                    if (f instanceof Folder) {
                        scanFolder(f, rootPath, groups);
                    }
                    else if (f instanceof File) {

                        var name = f.name;
                        if (!name.match(/\.jsx$/) && !name.match(/\.jsxbin$/)) continue;
                        if (f.fsName === File($.fileName).fsName) continue;

                        var folderName;
                        if (folderPath === rootPath) {
                            folderName = "Root Folder";
                        } else {
                            folderName = folderPath.substring(rootPath.length + 1);
                        }

                        if (!groups[folderName]) groups[folderName] = [];
                        groups[folderName].push(f);
                    }
                }
            }

            function refreshList() {

                var prevSelectionIndex =
                    (scriptList.selection && scriptList.selection.scriptIndex !== undefined)
                        ? scriptList.selection.scriptIndex
                        : -1;

                scriptList.removeAll();
                scriptItems = [];

                var rootPath = hardDecode(rootFolder.fsName);
                var groups = {};

                scanFolder(rootFolder, rootPath, groups);

                var folders = [];
                for (var k in groups) folders.push(k);
                folders.sort(sortAlpha);

                var index = 0;

                for (var i = 0; i < folders.length; i++) {

                    var folderName = folders[i];
                    scriptList.add("item", "[" + folderName + "]");

                    var scripts = groups[folderName];

                    scripts.sort(function (a, b) {

                        var aName = hardDecode(a.name).toLowerCase();
                        var bName = hardDecode(b.name).toLowerCase();

                        if (aName < bName) return -1;
                        if (aName > bName) return 1;
                        return 0;
                    });

                    for (var j = 0; j < scripts.length; j++) {

                        var file = scripts[j];
                        var cleanName = hardDecode(file.name);

                        var hk = getHotkeyIndexByFile(file.fsName);
                        var prefix = (hk >= 0) ? "(" + hk + ") " : "    ";

                        var item = scriptList.add("item", prefix + cleanName);

                        scriptItems[index] = file;
                        item.scriptIndex = index;

                        index++;
                    }
                }

                if (prevSelectionIndex >= 0 && scriptItems[prevSelectionIndex]) {
                    for (var i = 0; i < scriptList.items.length; i++) {
                        if (scriptList.items[i].scriptIndex === prevSelectionIndex) {
                            scriptList.selection = scriptList.items[i];
                            scriptList.active = true;
                            break;
                        }
                    }
                }

                if (firstRun) {

                    addLog("▶ Select a script above and click the \"Execute Script\" button or use hotkeys.");
                    addLog("▶ Hotkeys (0-9) can be assigned by selecting a script and clicking a \"Assign Hotkey\" button.");
                    addLog("▶ IMPORTANT: After pressing a keyboard hotkey then you must click in the \"Scripts Listing\" area for a subsequent hotkeys to function.");
                    addLog(" ");

                    firstRun = false;
                }

                addLog("Indexed: " + rootPath);
            }

            scriptList.onChange = function () {
                selectedItem = scriptList.selection;
            };

            function executeFile(file) {
                try {
                    $.evalFile(file);
                    addLog("Executed: " + file.name);
                } catch (e) {
                    addLog("Error: " + e.toString());
                }
            }

            function assignHotkey(num) {

                if (!selectedItem || selectedItem.scriptIndex === undefined) {
                    alert("Select a script first.");
                    return;
                }

                var file = scriptItems[selectedItem.scriptIndex];
                hotkeyMap[num] = file.fsName;

                addLog("Assigned hotkey " + num + " → " + file.name);
                refreshList();
            }

            for (var h = 0; h < 10; h++) {
                hotkeyButtons[h].onClick = function () {
                    assignHotkey(this.hotkeyIndex);
                };
            }

            executeBtn.onClick = function () {

                var sel = scriptList.selection;

                if (!sel || sel.scriptIndex === undefined) {
                    alert("Please select a script.");
                    return;
                }

                executeFile(scriptItems[sel.scriptIndex]);
            };

            pal.onKeyDown = function (event) {

                var k = event.keyName;

                if (k >= "0" && k <= "9") {

                    var idx = parseInt(k, 10);

                    if (hotkeyMap[idx] !== null) {

                        var f = File(hotkeyMap[idx]);

                        if (f.exists) {
                            executeFile(f);
                        } else {
                            addLog("Hotkey " + idx + " file missing.");
                        }
                    }
                }
            };

            pal.addEventListener("keydown", pal.onKeyDown);

            changeDirBtn.onClick = function () {
                var newFolder = Folder.selectDialog("Select root folder to index scripts");
                if (newFolder) {
                    rootFolder = newFolder;

                    // SAVE PERSISTENT ROOT FOLDER
                    app.settings.saveSetting(SETTINGS_SECTION, SETTINGS_KEY, rootFolder.fsName);

                    addLog("Root changed: " + rootFolder.fsName);
                    refreshList();
                }
            };

            refreshBtn.onClick = refreshList;

            function resizeUI() {

                var w = pal.size.width;
                var h = pal.size.height;
                var margin = 6;
                var spacing = 10;
                var hotkeyH = 50;

                hotkeyPanel.bounds = [
                    margin,
                    margin,
                    w - margin,
                    margin + hotkeyH
                ];

                var innerW = (w - margin * 2);
                var btnW = innerW / 10;

                for (var i = 0; i < 10; i++) {
                    hotkeyButtons[i].bounds = [
                        i * btnW,
                        10,
                        (i + 1) * btnW - 2,
                        hotkeyH - 10
                    ];
                }

                var topOffset = margin + hotkeyH + spacing;
                var buttonH = 30;
                var usableH = h - topOffset - buttonH - spacing * 2;
                var listH = Math.max(120, usableH * 0.65);
                var logH = usableH - listH;

                listPanel.bounds = [
                    margin,
                    topOffset,
                    w - margin,
                    topOffset + listH
                ];

                scriptList.bounds = [
                    5, 10,
                    listPanel.bounds[2] - listPanel.bounds[0] - 5,
                    listPanel.bounds[3] - listPanel.bounds[1] - 5
                ];

                buttonGroup.bounds = [
                    margin,
                    listPanel.bounds[3] + spacing,
                    w - margin,
                    listPanel.bounds[3] + spacing + buttonH
                ];

                var totalW = buttonGroup.bounds[2] - buttonGroup.bounds[0];
                var btn3 = totalW / 3;

                changeDirBtn.bounds = [0, 0, btn3 - 4, buttonH];
                refreshBtn.bounds = [btn3, 0, btn3 * 2 - 4, buttonH];
                executeBtn.bounds = [btn3 * 2, 0, totalW, buttonH];

                logPanel.bounds = [
                    margin,
                    buttonGroup.bounds[3] + spacing,
                    w - margin,
                    buttonGroup.bounds[3] + spacing + logH
                ];

                logBox.bounds = [
                    5, 10,
                    logPanel.bounds[2] - logPanel.bounds[0] - 5,
                    logPanel.bounds[3] - logPanel.bounds[1] - 5
                ];
            }

            pal.onResizing = pal.onResize = resizeUI;

            refreshList();

            pal.layout.layout(true);
            resizeUI();

            pal.active = true;

            return pal;
        }

        var ui = buildUI(thisObj);

        if (ui instanceof Window) {
            ui.center();
            ui.show();
        } else {
            ui.layout.layout(true);
        }

    } catch (err) {
        alert("Error:\r\n" + err.toString());
    } finally {
        app.endUndoGroup();
    }

})(this);
