Skip to content

Instantly share code, notes, and snippets.

@BeSpunky
Created May 7, 2023 20:13
Show Gist options
  • Save BeSpunky/468b2e790ba9e32a73a3717dc876bdc4 to your computer and use it in GitHub Desktop.
Save BeSpunky/468b2e790ba9e32a73a3717dc876bdc4 to your computer and use it in GitHub Desktop.
ScriptKit Script: Selected Scripts
// Name: Multi-Select Example
// Author: Shy Agam
// Twitter: @shyagam
// GitHub: https://github.com/BeSpunky
import "@johnlindquist/kit"
import { multiArg } from '../lib/ui'
import { identity, scriptChoices } from '../lib/utils';
const choices = await scriptChoices({ valueAs: 'command' });
const c = await multiArg({
placeholder: 'Select some options',
}, choices, { styleSelected: choice => `👉 <h3 class="ml-2 animate-bounce">${ choice.name }<h3>`});
inspect(c);
import { Choice, PromptConfig } from '@johnlindquist/kit/types/core';
import { identity } from './utils';
type MultiSelectConfig = {
styleSelected?: <T>(choice: Choice<T>) => string;
styleUnselected?: <T>(choice: Choice<T>) => string;
};
export async function multiArg(
...[placeholderOrConfig, choicesOrPanel, multiConfig]: [...Parameters<typeof arg>, MultiSelectConfig?]
)
{
const defaultHtml = (emoji: string, { name, description }: Choice) => /* html */`
<div class="flex flex-row overflow-x-hidden items-center h-full">
<span class="mr-2">${ emoji }</span>
<div class="flex flex-col max-w-full overflow-x-hidden max-h-full">
<div class="text-base truncate"><span>${ name }</span></div>
<div class="pb-1 truncate text-xs opacity-60"><span>${ description }</span></div>
</div>
</div>
`;
const config = {
styleSelected: (choice) => choice.html ? `✅ ${ choice.html }` : defaultHtml('✅', choice),
styleUnselected: (choice) => choice.html ? `🔹 ${ choice.html }` : defaultHtml('🔹', choice),
...multiConfig
} satisfies MultiSelectConfig;
let selectedChoiceIds = new Set<string>();
let selecting = true;
if (typeof placeholderOrConfig === 'string')
placeholderOrConfig = { placeholder: placeholderOrConfig };
const patchedOptions = {
...placeholderOrConfig,
// Change the description of the Enter key
enter: 'Toggle Selection',
// Add a shortcut to accept the current selection
shortcuts: [
...(placeholderOrConfig.shortcuts ?? []),
{
key: 'ctrl+enter',
name: 'Accept',
bar: 'right',
onPress()
{
selecting = false;
submit('');
}
}
]
} satisfies PromptConfig;
async function produceChoices(): Promise<Choice<any>[]>
{
const choicesSource = choicesOrPanel ?? patchedOptions.choices;
if (!choicesSource) return [];
const rawChoices = await (typeof choicesSource === 'function' ? choicesSource('') : choicesSource);
const stringToChoice = (choice: string): Choice => ({ id: choice, name: choice, value: choice });
if (Array.isArray(rawChoices) && rawChoices.length > 0)
{
return typeof rawChoices[0] === 'string'
? (rawChoices as string[]).map(stringToChoice)
: rawChoices as Choice[];
}
return rawChoices ? [stringToChoice(rawChoices as string)] : [];
}
const allChoices = await produceChoices();
const choiceId = (choice: Choice) => choice.id ?? choice.name;
const choicesById = new Map(
allChoices.map(choice => [choiceId(choice), choice])
);
async function filterAndPatchChoices(input: string): Promise<Choice<any>[]>
{
const loweredInput = input.toLowerCase();
const hasInput = (choice: Choice) => choice.name.toLowerCase().includes(loweredInput) || choice.description?.includes(loweredInput);
const filteredChoices = allChoices.filter(hasInput);
return filteredChoices.map((choice) =>
{
return {
...choice,
html: selectedChoiceIds.has(choiceId(choice))
? config.styleSelected(choice)
: config.styleUnselected(choice),
};
});
}
while (selecting)
{
const choiceValue = await arg(patchedOptions, filterAndPatchChoices);
if (!choiceValue) continue;
// Set the preselected value to the last choice, so the next arg call would stay focused on it
patchedOptions.defaultValue = choiceValue;
const id = choiceId(allChoices.find(choice => _.isEqual(choice.value, choiceValue)));
selectedChoiceIds.has(id) ? selectedChoiceIds.delete(id) : selectedChoiceIds.add(id);
}
const selectedValues = [...selectedChoiceIds.values()].map(id => choicesById.get(id)).map(choice => choice.value);
return selectedValues;
}
import type { Choice, Script } from '@johnlindquist/kit';
import type { TsFilePath } from './types/paths.types';
export function identity<T>(value: T): T { return value; }
export function getRandomItemFromArray<T>(array: T[]): T
{
return array[Math.floor(Math.random() * array.length)];
}
export function randomId(): number
{
return Math.round(Math.random() * 1000000000);
}
export function isTsFilePath(fileName: string): fileName is TsFilePath
{
return fileName.endsWith('.ts');
}
export function isTsScript(script: Script): script is Script & { filePath: TsFilePath }
{
return isTsFilePath(script.filePath);
}
export function ensureIsTsFilePath(path: string, message?: string): asserts path is TsFilePath
{
if (!isTsFilePath(path))
{
console.log(message ?? `🤔 '${ path }' is not a ts file. Exiting...`);
exit();
}
}
export function ensureIsWatchChangeEvent(event: string): asserts event is 'change'
{
if (event !== 'change')
{
console.log(`🤔 '${ event }' is not a watch change event. Exiting...`);
exit();
}
}
interface ScriptChoicesConfig<ValueAs extends keyof Script | ((script: Script) => any)>
{
scripts: Script[];
valueAs: ValueAs;
tsOnly: boolean;
addPreview: boolean
}
type ScriptChoice<Value extends keyof Script | ((script: Script) => any)> = Choice<
Value extends keyof Script ? Script[Value] :
Value extends (script: Script) => infer Return ? Return : never
>;
export async function scriptChoices<ValueAs extends keyof Script | ((script: Script) => any)>(
{
scripts,
valueAs,
tsOnly = true,
addPreview = true
}: Partial<ScriptChoicesConfig<ValueAs>> = {}
): Promise<ScriptChoice<ValueAs>[]>
{
scripts ??= await getScripts();
const createValue = (script: Script) =>
typeof valueAs === 'function'
? valueAs(script)
: script[valueAs as keyof Script ?? 'filePath'];
return (tsOnly ? scripts.filter(isTsScript) : scripts)
.map(script => ({
id: script.name,
name: script.name,
description: script.description,
value: createValue(script),
preview: addPreview ? () => highlightScript(script) : undefined
}) satisfies ScriptChoice<ValueAs>);
}
export async function highlightScript(script: Script, language: string = 'ts'): Promise<string>
{
const scriptContent = await readFile(script.filePath, 'utf-8');
const markdown = `
\`\`\`${ language }
${ scriptContent }
\`\`\`
`;
return highlight(md(markdown, 'p-0 m-0 h-full w-full'), 'h-full w-full');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment