Prompt With List... On Steroids! 💪🏼

@noisneil, this is super cool; steroids for sure! :muscle: Thanks for sharing.

I have two questions/suggestions to consider:

  1. The native Prompt With List also supports multiple selection via macOS standard methods. Is there a reason you did not implement this standard?

  2. Light and Dark Mode keyed off DARKMODE() would be awesome.


You are certainly one of the forum experts with respect to Custom HTML Prompt. Thanks for sharing your expertise!

1 Like

Yes, because of the checkmark function. Perhaps I was wrong to proceed along that path, but I liked the idea of being able to perform multiple list-refining searches while maintaining a selection of items to be returned. I did try to implement both functions, but it caused a bunch of conflicts that I found too confusing to deal with.

...but I'm a glutton for punishment, so I had another go (see below). Still needs a bit of refinement, but I think it's pretty usable.

Done.

I've updated the initial post.

4 Likes

Updated initial post again to fix an issue with ⇧↑ that was driving me mad. Amazing how something as seemingly basic as getting selection expansion to work in both directions can get gnarly! Anyway, I think it's pretty good now!

4 Likes

:clap::clap::clap:

The latest version is very close to the macOS native behavior.

  • Unlike native shift-clicking, your dialog allows one to select two or more non-contiguous ranges. IMHO, that's better than the macOS native behavior.

  • Deselecting a selected (aka checked) range is not possible. That's a slight disadvantage, but from a practical perspective, not really a problem.

Overall, incredibly well done, @noisneil!

Thank you; I love this. I'm one of the weirdos that generally favors light mode.

I agree with @tiffle's preference here. I was able to narrow the sides easily, but the top and bottom seem less obvious:

    var inputWidth = promptSize[0] - 40;
    var listWidth = promptSize[0] - 40;
    var listHeight = promptSize[1] - 135;
2 Likes

Wow … holy cow … this is crazy stuff !

@noisneil your skills with the Custom HTML Prompt are amazing…

Greetings from Germany

Tobias

2 Likes

Updated to include a submacro that dismisses the prompt if the user clicks away from it.

Also updated: Narrower border in response to:

Also updated: User can now click and drag to mark or unmark items. In response to:

CleanShot 2023-09-16 at 10.37.11

5 Likes

Thanks, @noisneil. The gift that keeps on giving. :grinning:

Nice enhancement!

Thanks. I like that the prompt doesn't obscure as much screen footprint. Also, IMHO, the prompt is more attractive.

Not exactly like the native behavior, but the way you've implemented this is very intuitive.


I noticed that you are keying off KMDARKMODE() rather than DARKMODE(). That's the way the native Prompt With List works and probably better than my above suggestion. Thanks.


@noisneil, this is one of the most powerful Custom HTML Prompts that I've seen. If you ever get the extra time, a significantly commented version of the HTML would be an amazing entry in the Tips & Tutorials section of the forum. Also, I'm sure others would love to hear what resources you used to gain this expertise!

4 Likes

Definitely second that :+1: :heavy_plus_sign:

1 Like

@noisneil, I really like the latest version. However, I noticed the Page Up/Down does not scroll the list anymore.

I agree. It was fairly low on my list of things to sort out, so ended up not getting done before you and @tiffle nudged me.

Then I must have misunderstood. What do you mean by 'deselect a checked range'? In this latter version, there is no 'selection' except for the current highlighted line. ⇧↑/↓ marks adjacent items, as does clicking and dragging. At no point is more than one item selected.

I've been waiting for this moment. The big reveal... Drum roll please...

I dun told y'all!

I don't know the first thing about HTML! :joy:

Well... that's not actually true any more, as this process has been pretty much the most comprehensive and immersive learning experience I could hope for. I can now say I know about the first two dozen things about HTML... Maybe even three!

With careful prompting and an organised mind, Bing chat has walked me through implementing every feature of this script. It doesn't always get it right, but mostly it does. At a certain point, the script became so complex that Bing would get a bit confused. Poor ol' Bing. So I subscribed to Chat GPT Plus, giving me access to GPT 4, which is far more capable.

Now, none of this is to say that a LOT of work wasn't required on my part. Countless prompts, failures, conflicts and more prompts... Quite early on, the script quickly became too long to post back into Bing Chat, so I had to learn to organise all the functions and just ask for script portions that did this or that. Conflicts are always tricky because they do require me to understand the logic of the script, even if I'm not conversant in the language of it. There's probably some redundant code in there somewhere, but right now it works, so I'm not too tempted to mess with it until something breaks.

Most macros I put together now use AI generated shell scripts to replace bloated and tiresome groups of actions with just one. The reaction to my post about Bing Chat (linked above) was fairly muted, given the revolutionary possibilities for us as automation warriors. Perhaps this example will inspire others to take the plunge into AI script creation. The sky's the limit with this stuff!

Fixed now.

5 Likes

Well I guess I really meant, "Deselect or shrink a range."

In the following animation, the Shift key is down after the Sure, Ken row is clicked.

2023-09-16 23.05.56

@noisneil, thanks for sharing the approach you've used. I particularly appreciate the details regarding the AI tools and their strengths and constraints.

Curious, how does the speed compare to the free version of Chat GPT?


Maybe you could ask Chat GPT 4 to add the verbose comments. :wink:

I'll second that.

My experience using the free version of ChatGPT to help me with Xcode and ObjC/Swift app programming has been frustrating. Repeatedly, when I ask for how to do something, it has given me functions that look right syntactically, but using features, methods, parameters, etc. that have the right-sounding names but which do not exist.

Have you encountered that and do you have any suggestions for how to avoid it?

A friend suggested asking ChatGPT to ask me three clarifying questions before answering, and that has sometimes helped.

To help me keep track of my prompts and responses, I wrote some simple KBM text replacement macros that let me add a prompt number to the beginning of each prompt, and reset what the next number will be if necessary, and end each prompt with a date and time string, for my own reference.

Have you adjusted this behaviour or is this an old version I posted? In a previous iteration I worked on, the user would highligh a portion of the list and then hit ⇧↵ to mark the "selection". However, I realised that this is one step more than is necessary when marking items (which is likely to be a more frequent activity in practical terms than unmarking), so I removed the highlighting step:

CleanShot 2023-09-17 at 08.58.15

It's frighteningly fast to begin its reply and seems to handle complexity better. On the other hand, there is a limit to the number of questions you can ask within a 2hr window, which seems a little off considering it's a paid service.

I tried that, but as I mentioned earlier, the script is far too long to post back into the chat. I am able to post it in chunks, and GPT is able to understand it, but the likelihood of error increases with complexity. Here's the result of my asking for a commented version. No matter how I plead with it, GPT won't give it to me without code placeholders. When I've tried to fill in the blanks, I've ended up with a broken prompt.

I just tried asking for verbose comments, section by section, and here's the result (which is broken):

Script
<!DOCTYPE html>
<html>

<head>
    <title>Prompt With List (HTML)</title>
    <style id="dynamic-styles">
        /* This section is reserved for styles that are added dynamically at runtime */
        /* Disallow text selection when dragging, enhancing the user experience */
        body.dragging {
            user-select: none;
        }
    </style>
    <script>
        // Create a reference to KeyboardMaestro, this helps avoid unnecessary global scope pollution
        var KeyboardMaestro = window.KeyboardMaestro;

        // Initialize an array to keep track of indices of selected items
        var selectedIndices = [];

        // Initialize an array to hold the selected items themselves
        var selectedItems = [];

        // A flag to determine if an item is currently being dragged
        var isDragging = false;

        // A flag to determine if an item is being marked or selected
        var marking = false;

        /**
         * Toggle the selection of a given item.
         * @param {HTMLElement} item - The list item to select or unselect.
         * @param {boolean} shouldSelect - Explicit instruction to select (true) or unselect (false). If undefined, toggle.
         */
        function selectItem(item, shouldSelect) {
            // Convert the NodeList of 'li' elements to an array and get the index of the provided item
            var itemIndex = Array.from(document.querySelectorAll('li')).indexOf(item);

            // Check if the item's index is already in the selectedIndices array
            var index = selectedIndices.indexOf(itemIndex);

            if (index > -1) {
                // If the item is already selected
                if (shouldSelect !== true) {
                    // If the function was called with explicit instruction not to select, remove the item from selections
                    selectedIndices.splice(index, 1);
                    item.firstChild.checked = false; // Uncheck the checkbox associated with the item
                }
            } else {
                // If the item is not currently selected
                if (shouldSelect !== false) {
                    // If the function was called without explicit instruction to unselect, add the item to selections
                    selectedIndices.push(itemIndex);
                    item.firstChild.checked = true; // Check the checkbox associated with the item
                }
            }
            
            // Focus on the input element of type text, likely for further user input or interactions
            document.querySelector('input[type="text"]').focus();

            // Highlight the clicked item for visual feedback
            var selectedItem = document.querySelector('li.selected');
            if (selectedItem) {
                // If an item is already highlighted, remove the 'selected' class
                selectedItem.classList.remove('selected');
            }
            // Add the 'selected' class to the current item
            item.classList.add('selected');
        }

        // Initialize variables to keep track of the starting and ending items when using shift+click for range selection
        var shiftSelectStartItem = null;
        var shiftSelectEndItem = null;
/**
 * Handle keydown events for navigation and actions within the list.
 * @param {Event} event - The keydown event.
 */
function keydown(event) {
    // Convert the list items NodeList to an array
    var items = Array.from(document.querySelectorAll('li'));
    // Define keys of interest
    var keys = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape', 'PageUp', 'PageDown'];
    
    // Prevent default actions for these key events to ensure custom behaviors
    if (keys.includes(event.key)) {
        event.preventDefault();
    }
    
    // Find the currently selected item
    var selectedItem = document.querySelector('li.selected');
    // Filter out non-visible list items
    var visibleItems = items.filter(function(item) { return item.style.display !== 'none'; });
    // Get the index of the currently selected item within the visible items array
    var selectedIndex = visibleItems.indexOf(selectedItem);

    // Handle arrow keys for navigation within the list
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
        // Calculate the new index based on the arrow key pressed
        var newIndex = event.key === 'ArrowDown' ? selectedIndex + 1 : selectedIndex - 1;
        // Check if the new index is within bounds
        if (newIndex >= 0 && newIndex < visibleItems.length) {
            // If shift key is pressed, mark a range of items
            if (event.shiftKey) {
                if (!window.selectedItemStart) {
                    window.selectedItemStart = selectedItem;
                }
                markRange(window.selectedItemStart, visibleItems[newIndex]);
            } else {
                removeHighlightFromAll();
            }

            // Update the selected state
            selectedItem.classList.remove('selected');
            visibleItems[newIndex].classList.add('selected');
            scrollSelectedItemIntoView();
            window.selectedItemStart = visibleItems[newIndex];
        }
    } 

    // Handle Enter key for marking items or submitting the selection
    else if (event.key === 'Enter') {
        // If shift key is pressed, handle range selection
        if (event.shiftKey) {
            if (shiftSelectStartItem && shiftSelectEndItem) {
                // Logic to determine if all visible items in the range are marked
                var allMarked = true;
                var visibleIndices = visibleItems.map(function(visibleItem) {
                    return Array.from(document.querySelectorAll('li')).indexOf(visibleItem);
                });
                var startIndex = visibleIndices.indexOf(selectedIndices[selectedIndices.length - 1]);
                var endIndex = visibleIndices.indexOf(visibleIndices[visibleIndices.length - 1]);
                
                // Check each item in the range
                for (var i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) {
                    if (!document.querySelectorAll('li')[visibleIndices[i]].firstChild.checked) {
                        allMarked = false;
                        break;
                    }
                }

                // Toggle the mark state for items in the range
                for (var i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) {
                    selectItem(document.querySelectorAll('li')[visibleIndices[i]], !allMarked);
                }
            } else {
                // If no range is defined, just mark the single selected item
                selectItem(selectedItem);
            }
        } else {
            // Handle single or multiple item selection and submit
            if (selectedIndices.length === 0) {
                selectedIndices.push(Array.from(document.querySelectorAll('li')).indexOf(selectedItem));
            }

            // Sort selectedIndices based on the original order in promptList
            var promptList = KeyboardMaestro.GetVariable('Local__Prompt List').split('\n');
            selectedIndices.sort(function(a, b) {
                return a - b;
            });

            // Convert indices back to their respective items
            var selectedItems = selectedIndices.map(function(index) {
                return document.querySelectorAll('li')[index].dataset.fullText;
            }).join('\n');

            // Set and submit the selected items
            KeyboardMaestro.SetVariable('Local__PromptChoice', selectedItems);
            KeyboardMaestro.Submit('OK');
        }
    } 

    // Handle Escape key for canceling
    else if (event.key === 'Escape') {
        KeyboardMaestro.Submit('Cancel');
    }

    // Handle Page Up and Page Down for quick scrolling
    if (event.key === 'PageUp' || event.key === 'PageDown') {
        var list = document.querySelector('ul');
        var visibleHeight = list.offsetHeight; // Get the currently visible height of the ul

        // Adjust the scroll position based on the key pressed
        if (event.key === 'PageUp') {
            list.scrollTop -= visibleHeight;
        } else if (event.key === 'PageDown') {
            list.scrollTop += visibleHeight;
        }

        // Prevent default behavior
        event.preventDefault();
    }
}


/**
 * Handle the logic for when an item in the list is clicked.
 * @param {Event} event - The click event object.
 */
function handleItemClick(event) {
    if (isDragging) {
        return; // If currently dragging an item, exit without performing any action.
    }
    
    var clickedItem = event.currentTarget; // Get the item that was clicked.
    var selectedItem = document.querySelector('li.selected'); // Find the currently selected item, if any.

    // If the shift key is pressed and there's a starting item for range selection
    if (event.shiftKey && window.selectedItemStart) {
        markRange(window.selectedItemStart, clickedItem); // Mark all items in the range.
    } else {
        // If the shift key wasn't pressed or no initial item was set, this clicked item becomes the starting point for future shift+click actions.
        window.selectedItemStart = clickedItem;

        // If there's a previously selected item, remove its selected status.
        if (selectedItem) {
            selectedItem.classList.remove('selected');
        }

        // Set the clicked item as the selected item.
        clickedItem.classList.add('selected');
    }
}

/**
 * Highlights a range of items between the start and end items.
 * @param {HTMLElement} startItem - The item where the range starts.
 * @param {HTMLElement} endItem - The item where the range ends.
 */
function highlightRange(startItem, endItem) {
    var items = Array.from(document.querySelectorAll('li'));
    var startIndex = items.indexOf(startItem);
    var endIndex = items.indexOf(endItem);

    // Clear highlights from all items.
    items.forEach(function(item) {
        item.classList.remove('highlighted');
    });

    // Apply the highlight to items within the defined range.
    for (var i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) {
        items[i].classList.add('highlighted');
    }
}

/**
 * Removes highlights from all items in the list.
 */
function removeHighlightFromAll() {
    var items = Array.from(document.querySelectorAll('li'));

    // Clear highlights from all items.
    items.forEach(function(item) {
        item.classList.remove('highlighted');
    });
}

/**
 * Marks a range of visible items between the start and end items.
 * @param {HTMLElement} startItem - The starting item of the range.
 * @param {HTMLElement} endItem - The ending item of the range.
 */
function markRange(startItem, endItem) {
    // Fetch all visible items from the list.
    let visibleItems = Array.from(document.querySelectorAll('li')).filter(function(item) {
        return item.style.display !== 'none';
    });
    
    let startIndex = visibleItems.indexOf(startItem);
    let endIndex = visibleItems.indexOf(endItem);

    // Ensure the start index is smaller than the end index.
    if (startIndex > endIndex) {
        [startIndex, endIndex] = [endIndex, startIndex]; // Swap the indices.
    }

    // Mark each item in the range.
    for (let i = startIndex; i <= endIndex; i++) {
        selectItem(visibleItems[i], true);
    }
}

/**
 * Filters the list based on the input provided by the user.
 */
function filterList() {
    // Fetch the filter text and convert it to lowercase for case-insensitive searching.
    var filter = document.querySelector('input').value.toLowerCase();
    
    // Split the input text into individual terms for multiple keyword searching.
    var terms = filter.split(' ');

    var items = document.querySelectorAll('li');

    // Loop through each list item.
    for (var i = 0; i < items.length; i++) {
        var itemText = items[i].textContent.toLowerCase(); // Get the text of the item and convert it to lowercase.
        
        // Check if every term in the filter is present in the item's text.
        if (terms.every(function(term) { return itemText.indexOf(term) > -1; })) {
            // If all terms are found within the item text, display the item.
            items[i].style.display = '';
        } else {
            // If any term is missing from the item text, hide the item.
            items[i].style.display = 'none';
        }
    }
}

   /**
 * Initializes the application, sets up the list based on the KeyboardMaestro variables, and attaches event listeners.
 */
function init() {
    // Start with no initial selected item.
    window.selectedItemStart = null;

    // Set the font and font-size for the body element from the KeyboardMaestro variables.
    document.body.style.setProperty('--font', KeyboardMaestro.GetVariable('Local__Prompt Font'));
    document.body.style.setProperty('--font-size', KeyboardMaestro.GetVariable('Local__Font Size') + 'px');
    
    // Extract the dimensions of the prompt from the KeyboardMaestro variable and calculate widths and heights.
    var promptSize = KeyboardMaestro.GetVariable('Local__Prompt Size').split(',');
    var inputWidth = promptSize[0] - 40;
    var listWidth = promptSize[0] - 40;
    var listHeight = promptSize[1] - 100;

    // Adjust the size of the input and set its placeholder.
    var input = document.querySelector('input[type="text"]');
    input.style.width = inputWidth + 'px';
    input.placeholder = KeyboardMaestro.GetVariable('Local__Placeholder'); // Set placeholder

    // Adjust the dimensions of the list.
    var list = document.querySelector('ul');
    list.style.width = listWidth + 'px';
    list.style.height = listHeight + 'px';

    // Process each line from the KeyboardMaestro prompt list variable.
    var promptList = KeyboardMaestro.GetVariable('Local__Prompt List').split('\n');
    for (var i = 0; i < promptList.length; i++) {
        var parts = promptList[i].split('__'); // Split each line at '__'
        
        // Create new list item and checkbox elements.
        var li = document.createElement('li');
        var checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.disabled = true; // Make sure checkbox is not interactive.
        li.appendChild(checkbox);

        // Add a space after the checkbox.
        var space = document.createTextNode(' '); 
        li.appendChild(space);

        // If '__' is present in the line, use the part after for display and store the part before in a data attribute.
        // If not, use the whole line for both display and output.
        var text;
        if (parts[1]) {
            text = document.createTextNode(parts[1]);
            li.dataset.fullText = parts[0];
        } else {
            text = document.createTextNode(parts[0]);
            li.dataset.fullText = parts[0];
        }
        li.appendChild(text);

        // Attach a click event listener to the list item.
        li.addEventListener('click', function(event) {
            selectItem(event.currentTarget); 
        });

        // Attach a double-click event listener to the list item.
        li.addEventListener('dblclick', function(event) {
            selectedItems = [event.currentTarget.dataset.fullText]; 
            KeyboardMaestro.SetVariable('Local__PromptChoice', selectedItems.join('\n'));
            KeyboardMaestro.Submit('OK');
        });

        // Add the created list item to the list.
        list.appendChild(li);
    }

    // Mark the first item in the list as selected.
    list.firstChild.classList.add('selected');

    // Add event listeners to handle input changes and keydown events.
    input.addEventListener('input', filterList);
    window.addEventListener('keydown', keydown);
    window.addEventListener('mouseup', handleMouseUp);

    bindClickEvents();
}

/**
 * Scrolls the list to ensure the selected item is visible in the view.
 */
function scrollSelectedItemIntoView() {
    // Get the selected item and the list container.
    var selectedItem = document.querySelector('li.selected');
    var list = document.querySelector('ul');

    // Calculate the positions and dimensions of the list and the selected item.
    var listRect = list.getBoundingClientRect();
    var itemRect = selectedItem.getBoundingClientRect();

    // Check if the selected item is below the middle of the view.
    if (itemRect.top > listRect.top + listRect.height / 2) {
        // Adjust the scroll position to bring the item to the middle.
        list.scrollTop += itemRect.top - listRect.top - listRect.height / 2 + itemRect.height / 2;
    } else if (itemRect.bottom < listRect.top + listRect.height / 2) {
        // If the selected item is above the middle of the view, adjust the scroll position to bring the item to the middle.
        list.scrollTop -= listRect.top + listRect.height / 2 - itemRect.bottom + itemRect.height / 2;
    }
}

// This function sets the theme of the page based on the user's preferred color scheme.
function setTheme() {
    // Check if the user's device prefers a dark color scheme.
    var isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    
    // Get the stylesheet where dynamic styles will be inserted.
    var styles = document.getElementById('dynamic-styles');
    
    // Check if the preferred color scheme is dark.
    if (isDarkMode) {
        // Insert styles for dark mode.
        styles.innerHTML = `
            /* Styles for the body element when the dark mode is active. */
body {
    /* Font styles are determined by CSS variables. */
    font-family: var(--font);
    font-size: var(--font-size);

    /* Background color is a near-black with 85% opacity. */
    background-color: rgba(0, 0, 0, 0.85);

    /* Text color is set to white. */
    color: white;

    /* Flex display for layout purposes. */
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Styling for text input elements in dark mode. */
input[type="text"] {
    width: 500px;
    padding: 12px; /* Padding has been updated recently. */
    margin: 10px 0;
    box-sizing: border-box; /* This makes the padding and border included in the total width and height. */
    border: none;
    /* Explicitly setting the bottom border to none. */
    border-bottom: none;
    background-color: #3a3a3c;
    color: white; /* Text color was added to be white. */
    font-size: var(--font-size);
    outline: none; /* Removes the browser default focus outline. */
}

/* Styling for unordered lists. */
ul {
    list-style-type: none; /* Removes the default bullet points. */
    margin: 0;
    padding: 0;
    width: 500px;
    height: 425px; /* Height has been updated recently. */
    overflow-y: auto; /* Allows for vertical scrolling if the content exceeds the height. This was added recently. */
}

/* Styling for list items. */
li {
    padding: 6px; /* Padding was updated recently. */
    cursor: pointer; /* Changes the cursor to a hand pointer on hover. */
    font-size: var(--font-size);
    line-height: 1; /* Ensures consistent line height. */
    white-space: nowrap; /* Prevents the text from wrapping to the next line. */
    overflow: hidden; /* Cuts off any overflow content. */
    text-overflow: ellipsis; /* If the content is cut off, it ends with an ellipsis (three dots). */
}

/* Styling for a selected list item. */
li.selected {
    background-color: #3a3a3c; /* Changes the background color. */
}

/* Styling for a highlighted list item. */
li.highlighted {
    background-color: #3a3a3c; /* This color can be customized as needed. */
}

/* Ensures that list items and the body element (when dragging) cannot be selected by the user. */
li, body.dragging {
    user-select: none; 
}

/* Styling for checkboxes. */
input[type="checkbox"] {
    position: relative;
    appearance: none; /* Removes the browser default appearance. */
    background-color: transparent;
    color: white;
    margin-right: 10px; /* Provides space to the right. */
    width: 3px; /* Explicitly sets a narrow width. */
    height: 6px; /* Explicitly sets a short height. */
    vertical-align: middle; /* Vertically centers the checkbox. */
}

/* Styling for checked checkboxes. This creates a custom checkmark appearance. */
input[type="checkbox"]:checked::before {
    content: "";
    position: absolute;
    top: calc(50% - 2px);
    left: 50%;
    transform: translate(-50%, -50%) rotate(45deg);
    display: inline-block;
    width: 3px; 
    height: 6px;
    border-width: 0 2px 2px 0;
    border-style: solid;
    border-color: white;
}

/* Scrollbar styles specific to WebKit browsers. */
::-webkit-scrollbar {
    width: 12px;
}

::-webkit-scrollbar-track {
    border-radius: 10px; /* Gives the track a rounded appearance. */
    background: #2f2f2f; 
}

::-webkit-scrollbar-thumb {
    border-radius: 10px; /* Gives the thumb (the draggable part of the scrollbar) a rounded appearance. */
    background: #888; 
}

/* Changes the thumb color when hovered over. */
::-webkit-scrollbar-thumb:hover {
    background: #555; 
}
            }
        `;
    } else {
        // Insert styles for light mode.
        styles.innerHTML = `
            /* Styles for the body element when the light mode is active. */
body {
    /* Font styles are determined by CSS variables. */
    font-family: var(--font);
    font-size: var(--font-size);

    /* The background color is a light gray with 95% opacity. */
    background-color: rgba(192, 192, 192, 0.95);

    /* Text color is set to a darker gray. */
    color: #444444;

    /* Utilizing flex for layout alignment purposes. */
    display: flex;
    flex-direction: column;
    align-items: center;
}

/* Styling specific to text input elements in light mode. */
input[type="text"] {
    width: 500px;
    padding: 12px; /* Updated padding value. */
    margin: 8px 0; /* Margin differs from dark mode. */
    box-sizing: border-box;
    border: none;
    /* Explicitly setting the bottom border to none. */
    border-bottom: none;
    background-color: #FFFFFF;
    color: #444444; /* Text color matches the body text color. */
    font-size: var(--font-size);
    outline: none; /* Removes the browser default focus outline. */
}

/* Styling for unordered lists. */
ul {
    list-style-type: none; /* No bullets. */
    margin: 0;
    padding: 0;
    width: 500px;
    height: 425px; /* Updated height value. */
    overflow-y: auto; /* Enables vertical scrolling. */
}

/* Styling for list items. */
li {
    padding: 6px; /* Updated padding value. */
    cursor: pointer; /* Cursor appears as a hand on hover. */
    font-size: var(--font-size);
    line-height: 1; /* Consistent line height across list items. */
    white-space: nowrap; /* Text does not wrap to next line. */
    overflow: hidden; /* Conceals overflowing content. */
    text-overflow: ellipsis; /* Displays an ellipsis for overflowed content. */
}

/* Potential hover effect for list items - currently commented out. */
/* li:hover {
    background-color: #FFFFFF;
} */

/* Background color for selected list items. */
li.selected {
    background-color: #EFEFEF;
}

/* Background color for highlighted list items. */
li.highlighted {
    background-color: #EFEFEF; /* This color is customizable. */
}

/* Ensures list items and the body element (during a dragging action) cannot be selected. */
li, body.dragging {
    user-select: none;
}

/* Custom styling for checkboxes. */
input[type="checkbox"] {
    position: relative;
    appearance: none; /* Removes default browser styling. */
    background-color: transparent;
    color: white;
    margin-right: 10px; /* Extra space to the right of the checkbox. */
    width: 3px; /* Explicitly narrow width. */
    height: 6px; /* Explicitly short height. */
    vertical-align: middle; /* Centers the checkbox vertically. */
}

/* Styling for checked state of checkboxes, creating a custom blue checkmark. */
input[type="checkbox"]:checked::before {
    content: "";
    position: absolute;
    top: calc(50% - 2px);
    left: 50%;
    transform: translate(-50%, -50%) rotate(45deg);
    display: inline-block;
    width: 3px; 
    height: 6px;
    border-width: 0 2px 2px 0;
    border-style: solid;
    border-color: #3399FF; /* A shade of blue. */
}

/* Scrollbar styles specific to WebKit browsers. */
::-webkit-scrollbar {
    width: 12px;
}

::-webkit-scrollbar-track {
    border-radius: 10px; /* Rounded track. */
    background: #888;
}

::-webkit-scrollbar-thumb {
    border-radius: 10px; /* Rounded thumb. */
    background: #666666; /* Darker thumb. */
}

/* Darker thumb color when hovered over. */
::-webkit-scrollbar-thumb:hover {
    background: #555;
}
            }
        `;
    }
}

// Listen for changes in the user's preferred color scheme and update the theme accordingly.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setTheme);

// This function binds various click events to the list items.
function bindClickEvents() {
    // Get all list items on the page.
    var listItems = document.querySelectorAll('li');
    
    // Loop over each list item and bind events.
    listItems.forEach(function(item) {
        item.addEventListener('click', handleItemClick);
        item.addEventListener('mousedown', handleMouseDown);
        item.addEventListener('mouseup', handleMouseUp);
        item.addEventListener('mouseover', handleMouseOver);
    });
}

// Handle the mousedown event on a list item.
function handleMouseDown(event) {
    event.preventDefault();
    
    // Set the isDragging flag to true, indicating that dragging has started.
    isDragging = true;

    // Check or uncheck the checkbox inside the list item based on its current state.
    marking = !event.target.querySelector('input[type="checkbox"]').checked;
    
    // Mark or unmark the initial item where mousedown happened.
    var checkbox = event.currentTarget.querySelector('input[type="checkbox"]');
    checkbox.checked = marking;

    // Add a class to the body indicating dragging is happening.
    document.body.classList.add('dragging');
}

// Handle the mouseup event on a list item.
function handleMouseUp() {
    // Set the isDragging flag to false, indicating that dragging has ended.
    isDragging = false;

    // Remove the dragging class from the body.
    document.body.classList.remove('dragging');
}
 
// Handle the mouseover event on a list item.
function handleMouseOver(event) {
    // Only proceed if dragging is in progress.
    if (isDragging) {
        // Check or uncheck the checkbox inside the list item based on the marking flag.
        var checkbox = event.currentTarget.querySelector('input[type="checkbox"]');
        checkbox.checked = marking;
    }
}

<body 
    <!-- When the body is loaded, three functions are triggered: -->
    <!-- 1. The window size is adjusted based on the value of the "Local__Prompt Size" variable from KeyboardMaestro. -->
    <!-- 2. The theme is set using the setTheme() function. -->
    <!-- 3. The init() function is called to presumably initialize some other features or settings. -->
    onload='window.KeyboardMaestro.ResizeWindow(window.KeyboardMaestro.GetVariable("Local__Prompt Size")); setTheme(); init()'
>
    <!-- This is a spacing div. It adds a 5px vertical space for layout purposes. -->
    <!-- Note: The "updated" comment indicates that this div's style or its existence is probably a recent change. -->
    <div style="height: 5px;"></div>

    <!-- This is an input text field. -->
    <!-- When the user types into this field, the filterList() function is called to presumably filter or search through a list. -->
    <!-- The "autofocus" attribute ensures that this input field is focused on when the page loads, meaning the user can start typing immediately. -->
    <input type="text" oninput="filterList()" autofocus>

    <!-- An empty unordered list. This will likely be populated dynamically based on the input or other processes. -->
    <ul></ul>
</body>
</html>


Yes, I've had similar experiences. I have a bunch of macros for prompting the AI to try to avoid this. Here's an example for when I'm asking for an AppleScript and Bing Chat doesn't know how to grab a variable from KM:

This is how you get a variable from Keyboard Maestro to AppleScript:

set inst to system attribute "KMINSTANCE"
tell application "Keyboard Maestro Engine"
	set ASVar to getvariable "Local__KMVar" instance inst
end tell

There do seem to be a few things Bing Chat and GPT 3.5 regularly get wrong, and these corrective macros are useful in that regard. One irritating thing is that Osascript doesn't work on my system (no idea why), so when I want a shell script, I often begin the conversation with "Do not use osascript", and yet Bing Chat seems to see the mere inclusion of the word osascript as an instruction to use it. GPT4 does seem more 'together' with all this and only seems to struggle at all when a script becomes very very long.

Bing chat numbers the messages, so this isn't necessary. I only just started with GPT 4, and I may implement something similar to your method, as the question number limit is a bit anxious-making when you're close to a working code and you don't know how many tries you have left.

2 Likes

Hey Neil (@noisneil)

What if you make this as an optional param for those who want this to have enabled ? - just an idea …

Another Idea - having the highlight color based on system setting if not defined and a color palette based on strings like light- or dark- and the color appended to it when highlighting is enabled.

Also a color palette for each - the prompt border and background as well as an optional setting for a smaller border and also obtional a setting for a border radius would be nice.

That’s all from my side for now ….

Thanks for building such a great Macro

Have a nice Sunday :sunglasses:

Greetings from Germany :de:

Tobias

I've spent a lot of hours on this script now and my enthusiasm for creating a deep set of aesthetic controls was fairly low to begin with. It's not hard to open the html and change the colour values per user, so I don't think it's really worth my time. If you're amped up about the feature, you should see if Bing Chat can help you implement it.

How about this for an idea?

Those interested in a particular aesthetic for the prompt can take the time to set the theme attributes up how they want them and post the code here. I'll then add a function that switches between multiple themes based on their names.

Sound like a plan?

3 Likes

My intention was to offer you my ideas first - and then maybe discuss them with you …

Maybe this could be possible if you implement code for the management of themes based on their names - but as I am aware of would you have more work to do for all the possibilities (which are quite endless) to implement … if I remember it right we’re my ideas based on max 3 more Parameters as there are now whose would be applicable using JSON and the end user could set up these Params to their liking …

I am currently not able to do anything in HTML like you are … but it sometimes seems that I have a good intention of how something in the world of programming computers could be solved with the most less amount of code involved - what means that you would have to have less more work to do if you go this route on implementing the code for these features …

There will also be less more code for you to debug if it comes to issues in future updates for this macro or for KM - even when you implement a wider color palette using hex based color options - only with the restriction on leaving darker design accessible in darkmode and the lighter design accessible in lightmode.

Sometimes less more code which is more complex at first sight does a better job than code that seems complex and endless …

How does that sound to you ?!

Greetings from Germany :de:

Tobias

Hi @noisneil. I have the latest version of your macro and didn't change anything. :grinning: I was just demonstrating the native macOS behavior with the Keyboard Maestro Prompt With List. In this case clicking on the top row defines the single item selected (or top of the range). When Shift is down and a row below is selected, all items are selected. Then when a row "higher" is clicked (Shift down), the range is reduced. With your macro, the range can be reduced, but the mouse must be held down as the mouse is moved up.

This is very minor, but since you asked, I thought I'd clarify. I think it's great as you have implemented!

Oh, I thought you were referring to Bing AI.

Thanks! Hum, I wonder why it's broken. Regardless, the comments are still useful, even if they can't be used in the "working" action.


Thanks for all of your effort and knowledge sharing, @noisneil.

1 Like

:man_facepalming:t2: of course. Sorry, I read that on my phone. In a pub.

Thanks for clarifying. I'll have a go and see if I can add that in.

I was. Bing has a 4000 character limit for questions.

1 Like

@Nr.5-need_input I'm kinda done with the script for now, as it was a bit intense trying to make it work and I want to step away from it and clear my head.

2 Likes