Created
May 7, 2023 20:13
-
-
Save BeSpunky/468b2e790ba9e32a73a3717dc876bdc4 to your computer and use it in GitHub Desktop.
ScriptKit Script: Selected Scripts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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