Customizing Arg Choices

I use GitHub search all the time for things like searching config files for certain terms, whether it's a Karabiner file or a package JSON file or whatever. In ScriptKit, you can create a custom GitHub search specific to your needs.

First, grab the URL containing the information you need.

Create a new script in ScriptKit, and add the following code to open the URL:


let url = `https://github.com/search?q=karabiner`;
open(url);

When you run the script, it will open a new tab with the search results.

Now, let's customize the query by pulling it out and using await arg:


const query = await arg("Enter a GitHub search query:");
let url = `https://github.com/search?q=${query}`;

This allows you to enter a custom search query when running the script, and it will update the search results accordingly.

To store recent search queries, create a file path for the recent queries:


let recentFilePath = kenvPath("recent", "karabiner.txt");

Use ensureReadFile to create and read the file as a text file if it doesn't exist:


let content = await ensureReadFile(recentFilePath);

Split the content and filter out empty lines to get the recent queries:


let recents = content.split("\n").filter((line) => line.trim().length > 0);
let query = await arg("Enter a GitHub search query:", recents);

When you run the script and start typing, you'll notice that the submit button is disabled. This is because, by default, strict mode is enabled when passing an array to a prompt. In strict mode, you have to enter text that matches something from the list.

To disable strict mode, set strict to false:


let query = await arg(
{
placeholder: "Enter a GitHub search query:",
strict: false,
},
recents
);

Now, you can search for items that aren't in the list, and the submit button will be enabled.

Next, we need to write out the file. Use the writeFile function, passing in the recent file path and modifying the recents array by removing the query from the current results. Since recents is an array, you'll have to join the elements.


recents = [query, ...recents.filter((line) => line !== query)];
await writeFile(recentFilePath, recents.join("\n"));

Now, the search should work fine, and if you perform a recent search, the new item will be added to the history.

You may want to edit the text file to clear something from your history or modify the recents for any reason. To do this, add an extra action:


const OPEN_FILE = "Open karabiner recents file";
let choices = [...recents, OPEN_FILE];
let query = await arg(
{
placeholder: "Enter a GitHub search query:",
strict: false,
},
choices
);
if (query === OPEN_FILE) {
await edit(recentFilePath);
exit();
}

When you run the updated code, you'll see that "Open File" is now an option. And if you have edit the file those options will be available.

We need to do some grouping choices for better organization:


let choices = groupChoices(
[
...recents.map((name) => {
return {
name,
value: name,
group: "Recents",
};
}),
{
name: OPEN_FILE,
value: OPEN_FILE,
group: "Actions",
},
],
{
order: ["Recents", "Actions"],
}
);

By doing this, when we run the script, we'll see recents at the top and actions at the bottom. We can now type something existing like "button" to search for button two, or type "open" to open our file. Alternatively, we can type something new and hit enter to run a query with the new input.

In cases where the typed text doesn't have any results, we can still present information to the user. To do this, add the following code:


choices.unshift({
info: true,
name: "Hit enter to open GitHub search results",
});

Now, the "Hit enter" message will always be at the top, providing users with information on what's going to happen even if they type something that doesn't exist.

To make the placeholder text appear only when there are no search results, we can change the value attribute to miss. However, if you run this now and type something, it would pass the value of this miss choice as the submitted query. To avoid this, we'll add the skip attribute as well.


choices.unshift({
miss: true,
skip: true,
name: "Hit enter to open GitHub search results",
});

This means that the choice can't be selected, and the input will be passed instead. For example, if you type "JavaScript" and hit enter, the value "JavaScript" is passed in as the query since the miss choice was skipped.

To style the placeholder text:


choices.unshift({
miss: true,
skip: true,
name: "Hit enter to open GitHub search results",
nameClassName: "text-primary",
});

Alternatively, you can define custom CSS for the placeholder text.


let css = `
.red {
color: red;
}
`;

Now, update the nameClassName attribute to use the red class.

When you run the script and type something, the placeholder text will now appear in red.

Transcript

00:00 I use GitHub search all the time for things like searching config files for certain terms, whether it's a Karabiner file or a package JSON file or whatever. So in ScriptKit, you can grab this URL, which contains the information we need, and I'm going to create a GitHub search, and we'll make this specific to Karabiner.

00:18 Hit enter to create a new script. If you have multiple Kens, this will show up, I'll put it in my main Ken. Then I'll just drop this URL in here, and I can say open URL. So now when I run this script, this will pop open in a new tab with all of that information. Now, what I want to do is customize the query here.

00:36 So I'll pull this out, call this a query, and say that our query is await arg, enter a GitHub search query. That's fine. So now we can say search for button two, hit enter, and now you'll see button two is up there,

00:55 and we have all these results for button two. Now, because I often search for the same thing, let's store our recent queries. I'll say a recent file path is our Ken path, and next to our scripts folder, we'll put a recent folder, and we'll just call this Karabiner.txt, because we're just going to store these queries as new lines in this file.

01:14 We can use ensure read file on that recent file path. So if that file doesn't exist, it will create it and read it as a text file, and we'll just call this the content, and then we'll just say our recents, and it looks like copilot new. I wanted to split the content and filter out empty lines. So now we're going to pass our recents into here as an array,

01:34 and I have to show you if I try and run this, and anything I type, you'll see that submit is disabled, and that's because once you start passing in an array to your prompt, if you want to pass some input or a value that doesn't match the array, then you're going to have to disable strict mode.

01:53 So we'll say strict is false. It defaults to true, meaning you have to enter text that matches something from the list, but now that it's false, we can go ahead and search for button three, hit enter, and you'll see everything works just fine. So now we have to write out the file.

02:10 So write file and the recent file path, and we'll take the recents and remove the query from the current results. You can see that copilot knew what we were doing already. So I'll say recents here, and since this is an array, we'll have to join that. Now if I search for button seven, which probably won't exist,

02:30 you'll see the search was just fine, and now if we do our recent search, that button seven is in the search. Now let's add an extra action to this to be able to edit the text file in case we want to clear something from our history or just modify the recents for whatever reason. We'll create this value called open file,

02:48 and this can just be like open carabiner recents file, and now we can see that our choices are the recents and open file. We'll pass our choices instead of the recents, and then if the query is open file, edit that path and exit. Exit will prevent it from running the script any further,

03:07 and you'll see that once we run this, you'll see open file is an option. We'll go ahead and select that, and it's editing a recents file, so we can tweak that to button two. We'll say that this is hello, and now in our list, you'll see button two, hello, and open carabiner recents file, and to organize this a little bit more,

03:25 I'm going to group these where these are only names right now, but if I group these to return names, values, and group, we'll call this recents. This can be an object of name open file, value open file, group, and this will be like actions, and then around this array,

03:44 we're going to say group choices, and by grouping the choices, now once we open this, you'll see we have actions at the top, recents at the bottom, and instead of that order, it makes more sense for recents to be at the top. In the group config, we'll say that the order is recents at the top,

04:04 actions at the bottom. We run this, we see recents at the top, actions at the bottom. We can type something existing like button, it'll search for button two. We can type open and open our file, or we can type something new, hit enter, and it will query with the new.

04:22 Now you get into this interesting state of this when you've typed text that doesn't have any results, and you can present information to the user that you can still do something with that, and I'm going to come down here and say choices unshift, and this is going to be info true,

04:40 which means it will always be there as information, and the name can be hit enter to open GitHub search results, something like that is great. So now when I run this, you'll see that hit enter is at the top, so even if I type something that doesn't exist, but that will always be there as kind of information of what's going to happen.

05:00 So we can change this to appear only when there are no search results, and we do that by changing this to miss, but if you're to run this now, and I type something, this would pass the value of this miss choice as what's submitted to query, but since we don't want that, we're going to add skip as well,

05:19 meaning that choice can't be selected, and so it'll skip it and pass in the input. So now we can open this, we can type something like JavaScript, and you'll see that it shows that information, it's unstyled, and we'll get to that, but if I hit enter, then JavaScript is what's passed in since this was skipped. So to quickly style this,

05:37 I'm going to say name, class name, we'll say text primary, and that will give us the primary text color from this theme down here, or you could define some custom CSS, so we'll say CSS is something like red, we'll make this really obvious color red, we'll make red the class name, and pass in the CSS,

05:58 run our script, type in something, and you'll see it's now red.