What is the fastest way to quickly select items in a prompt with list action?

I have a macro which prompts the user to make several selections from lists which contain 10 or fewer items.

The macro contains multiple "prompt with list" actions. For example, the first one will prompt with a list of 10 colors. The second will prompt with a list of 10 model numbers, and so on.

However I would like to reduce the number of keystrokes needed to traverse these lists, i.e. so that instead of needing to select an item then press Enter to complete the decision, I can just press a number on the number pad to immediately make the selection and have the macro automatically move on to the next list.

What is the best way to do this?

1 Like

You will probably get some different answers. One will involve a Custom HTML action. One will involve using a button for each option. One might involve the Display Progess Bar button.

I’m at lunch right now so I can’t help.

1 Like

Another would involve two macro groups—one that activates another. Take a look at my ColorSetter macro for an example. This lets me quickly select a color for an action with a single keystroke.

-rob.

1 Like

I guess I was wrong about the KM Prompt action supporting single key shortcuts. (I thought it did, but now I think it doesn't.)

I can't even find a single key solution using AppleScript. However I didn't promise there would be such a solution.

No doubt there's a way to do this using Custom HTML actions. I'll ponder it for a bit.

Rob says there's a way to do it by using KM groups which activate and deactivate each other. That's handy, but I don't see how it will show you your options. I think you want visual prompts.

Using palettes would probably work too, and I may ponder that, but I'm not skilled with them.

EDIT: I found a way to do it with Custom HTML prompts. But I know that you don't want to mess with HTML directly, so I'll try to come up with a macro that converts your list of items into a Custom HTML prompt. It will look something like this:

image

image

1 Like

That's how ColorSetter works, with a palette in one group…

...and a controller macro to activate and deactivate the palette. You can use whatever text you like, obviously.

-rob.

1 Like

Very Cool. In any case, I'm still pondering the HTML solution. It's a lot of work, but I think I can handle it.

Here's my solution. (I admit, I got some help from AI, but I also had to do some work myself. It's just a basic solution to prove that it's possible. Adding fluff and colours and calculating the window size in advance is fluff that I didn't include at this time.)

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Choose an Option</title>
<style>
  body { font-family: system-ui, sans-serif; padding: 1.2rem; text-align:center; }
  .option { font-size:1.05rem; margin:0.25rem 0; }
  .hint { margin-top:0.8rem; color:#666; font-size:0.9rem; }
  #output { margin-top:0.8rem; font-weight:600; color: #0b61a4;}
</style>
</head>
<body>
  <h2>Press the first letter to choose</h2>
  <div id="options"></div>
  <div id="output"></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const optionsDiv = document.getElementById('options');
  const output = document.getElementById('output');

  // 1) Try to read the KM variable at runtime via the provided API
  let raw = '';
  try {
    if (window.KeyboardMaestro && typeof window.KeyboardMaestro.GetVariable === 'function') {
      raw = window.KeyboardMaestro.GetVariable('FruitOptions') || '';
    }
  } catch (e) {
    raw = '';
  }

  // 2) Fallback to literal token only if you intentionally put a token and KM will expand it:
  if (!raw) raw = "%Variable%FruitOptions%"; // harmless fallback

  // 3) If still empty, inform user
  if (!raw || !raw.trim()) {
    optionsDiv.textContent = "No options found in KM variable 'FruitOptions'.";
    return;
  }

  // 4) Split on commas or newlines and trim
  const list = raw.split(/\r?\n|,/).map(s => s.trim()).filter(Boolean);

  // 5) Build a map from first letter -> option (first occurrence wins)
  const map = {};
  list.forEach(item => {
    const key = item[0] ? item[0].toUpperCase() : '';
    if (!map[key]) map[key] = item;
    const el = document.createElement('div');
    el.className = 'option';
    el.textContent = `${key} — ${item}`;
    optionsDiv.appendChild(el);
  });

  // 6) Key handler
  document.addEventListener('keydown', e => {
    // ignore modifier combos
    if (e.altKey || e.ctrlKey || e.metaKey) return;
    const k = (e.key || '').toUpperCase();
    if (map[k]) {
      const choice = map[k];
      output.textContent = `You chose: ${choice}`;

      // set KM variable (if available)
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.SetVariable === 'function') {
          window.KeyboardMaestro.SetVariable('ChosenOption', choice);
        }
      } catch (ex) {}

      // submit back to KM (close prompt)
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.Submit === 'function') {
          window.KeyboardMaestro.Submit(choice);
        }
      } catch (ex) {}
    }
  });
});
</script>
</body>
</html>
2 Likes

Very useful code!

I was wondering:

  1. What causes the pop sound?
  2. On pressing the ESC key, the last chosen item is returned. How to reset the chosen item to naught?
    Select item from HTML prompt.kmmacros (4.5 KB)

I heard it too, before I uploaded it, and I think that's the sound macOS generates when an app doesn't accept a keystroke. I wasn't sure if that was the Custom HTML app, or the app that appears directly behind it on the screen (the one that appears frontmost when the Custom HTML window closes.) At this point I have no idea how to fix it.

Good catch. I didn't notice that. That's a bug in the Javascript, I guess. I can look at it, but there's only a 50% chance I can fix it.

Dunno if this will work as I'm away from home, but I ran it through Claude on my phone.


<!doctype html>

<html>
<head>
<meta charset="utf-8">
<title>Choose an Option</title>
<style>
  body { font-family: system-ui, sans-serif; padding: 1.2rem; text-align:center; }
  .option { font-size:1.05rem; margin:0.25rem 0; }
  .hint { margin-top:0.8rem; color:#666; font-size:0.9rem; }
  #output { margin-top:0.8rem; font-weight:600; color: #0b61a4;}
</style>
</head>
<body>
  <h2>Press the first letter to choose</h2>
  <div id="options"></div>
  <div id="output"></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const optionsDiv = document.getElementById('options');
  const output = document.getElementById('output');

  // 1) Try to read the KM variable at runtime via the provided API
  let raw = '';
  try {
    if (window.KeyboardMaestro && typeof window.KeyboardMaestro.GetVariable === 'function') {
      raw = window.KeyboardMaestro.GetVariable('FruitOptions') || '';
    }
  } catch (e) {
    raw = '';
  }

  // 2) Fallback to literal token only if you intentionally put a token and KM will expand it:
  if (!raw) raw = "%Variable%FruitOptions%"; // harmless fallback

  // 3) If still empty, inform user
  if (!raw || !raw.trim()) {
    optionsDiv.textContent = "No options found in KM variable 'FruitOptions'.";
    return;
  }

  // 4) Split on commas or newlines and trim
  const list = raw.split(/\r?\n|,/).map(s => s.trim()).filter(Boolean);

  // 5) Build a map from first letter -> option (first occurrence wins)
  const map = {};
  list.forEach(item => {
    const key = item[0] ? item[0].toUpperCase() : '';
    if (!map[key]) map[key] = item;
    const el = document.createElement('div');
    el.className = 'option';
    el.textContent = `${key} — ${item}`;
    optionsDiv.appendChild(el);
  });

  // 6) Key handler
  document.addEventListener('keydown', e => {
    // Handle Escape key to dismiss/cancel
    if (e.key === 'Escape') {
      output.textContent = 'Cancelled';
      
      // set KM variable to empty string
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.SetVariable === 'function') {
          window.KeyboardMaestro.SetVariable('ChosenOption', '');
        }
      } catch (ex) {}

      // submit empty string back to KM (close prompt)
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.Submit === 'function') {
          window.KeyboardMaestro.Submit('');
        }
      } catch (ex) {}
      return;
    }

    // ignore modifier combos
    if (e.altKey || e.ctrlKey || e.metaKey) return;
    const k = (e.key || '').toUpperCase();
    if (map[k]) {
      const choice = map[k];
      output.textContent = `You chose: ${choice}`;

      // set KM variable (if available)
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.SetVariable === 'function') {
          window.KeyboardMaestro.SetVariable('ChosenOption', choice);
        }
      } catch (ex) {}

      // submit back to KM (close prompt)
      try {
        if (window.KeyboardMaestro && typeof window.KeyboardMaestro.Submit === 'function') {
          window.KeyboardMaestro.Submit(choice);
        }
      } catch (ex) {}
    }
  });
});
</script>

</body>
</html>

1 Like

I wonder if there might be an easier solution using the shell? The 'nl' command is a line numbering filter which can be used to prefix numbers to lines which are fed to it. For example, when I run ls | nl -nrn -w3 -s '. ' in a given directory, I get the following output:

  1. drag-drop-table.html
  2. jalex.html
  3. latex-no-km.html
  4. latex.html
  5. Makefile
  6. presets

If you have fewer than 10 items, you could put all of those items into a variable (on separate lines), then use a shell command to add numerical prefixes (as per the above), and then feed that into the Prompt With List macro. Typing the numerical prefix will then allow you to select the item.

For example, if you have the list stored in a KM variable named 'testVar', then if you evaluate the following in the shell:

echo "$KMVAR_testVar" | nl -nrn -w3 -s '. '

You'll get a numbered list generated. You could then capture that in another KM variable, which you feed into the "Prompt with list" macro.

One "gotcha" is that the selected item will contain the numerical prefix, which is probably not what you want. So you will then need to strip off the numerical prefix in order to get the selected data, but that's doable via regex search and replace.

1 Like