// Name: GitHub Repos// Description: List GitHub repositories and perform actions on them// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const { Octokit } = await npm("octokit");const shodwon = await npm("showdown");const converter = new shodwon.Converter({ghMentions: true,emoji: true,});converter.setFlavor("github");interface RawRepositoryType {id: string;name: string;html_url: string;full_name: string;visibility: string[];description: string;homepage?: string;owner: {login: string;};open_issues_count: number;}interface RawIssueType {id: string;title: string;body: string;labels: {name: string;}[];number: string;user: {login: string;};html_url: string;}interface RawBranchName {id: string;name: string;protected: boolean;}interface OptionType<ValueType = unknown> {name: string;descritpion?: string;preview?: string;value: ValueType;}const auth = await env(`GITHUB_ACCESS_TOKEN`, "Enter your GitHub access token");const octokit = new Octokit({ auth });const {data: { login },} = await octokit.rest.users.getAuthenticated();const universalOptions = [{name: "Browse",description: "Open the url in your default browser",value: "browse",},{name: "Copy URL",description: "Copy the URL to the clipboard",value: "copy",},];const mapRawRepo = (repo: RawRepositoryType) => ({name: repo.full_name,description: [repo.visibility[0].toUpperCase() + repo.visibility.slice(1),repo.description,repo.homepage,].filter(Boolean).join(" ยท "),value: repo,});const mapReposResponse = (response: { data: RawRepositoryType[] }) =>(response.data || []).map(mapRawRepo);async function fetchAllRepos() {return await octokit.paginate(octokit.rest.repos.listForAuthenticatedUser,{ sort: "updated", per_page: 100 },mapReposResponse);}async function fetchRecentRepos() {const res = await octokit.request("GET /user/repos", {sort: "updated",per_page: 50,});return res.data;}async function fetchOwnerRepos() {const res = await octokit.request("GET /user/repos", {sort: "updated",per_page: 50,affiliation: "owner",});return res.data;}function createIssuePullHandler(key: "pull" | "issue") {return async (repo: RawRepositoryType) => {const res = await octokit.request(`GET /repos/{owner}/{repo}/${key}s`, {owner: repo.owner.login,repo: repo.name,pulls: key === "pull",});let { data: items } = res as { data: RawIssueType[] };if (key === "issue") {const pullsRes = (await octokit.request(`GET /repos/{owner}/{repo}/pulls`,{owner: repo.owner.login,repo: repo.name,})) as { data: RawIssueType[] };items = items.filter(({ number }) => !pullsRes.data.find((p) => p.number === number));}if (items.length === 0) {await div(`<div class="p-4 bg-white">No ${key}s</div>`);return;}const itemSelected = await arg(`Search for ${key}s`,items.map((i) => ({name: i.title,description: [`#${i.number}`,i.user.login,i.labels.map((l) => l.name).join(", "),].filter(Boolean).join(" ยท "),preview: `<style>p,h1,h2,h3 { margin-bottom: 8px; }</style><div class="p-4 bg-white"><h1>${i.title}</h1><small>By @${i.user.login} ยท ${i.labels.map((l) => l.name).join(", ")}</small><hr/><br />${i?.body ? converter.makeHtml(i.body) : "โ"}</div>`,value: i,})));const action = await arg("Select an action to perform", [...universalOptions,{name: `Copy ${key} number`,description: `Copy ${key} number to clipboard for reference elswhere (eg. branch-name)`,value: "number",},{name: `Close ${key}`,description: `Close ${key}`,value: "close",},]);switch (action) {case "browse":await browse(itemSelected.html_url);exit();case "copy":await copy(itemSelected.html_url);exit();case "number":await copy(`${itemSelected.number}`);exit();case "close":await await octokit.request(`PATCH /repos/{owner}/{repo}/${key}s/{${key}_number}`,{owner: repo.owner.login,repo: repo.name,[`${key}_number`]: itemSelected.number,state: "closed",});break;}notify(`${action} successful`);const nextHanlder = createIssuePullHandler(key);await nextHanlder(repo);};}async function getAllBranches(repo: RawRepositoryType) {const { data } = await octokit.request(`GET /repos/{owner}/{repo}/branches`, {owner: repo.owner.login,repo: repo.name,});const items = data as RawBranchName[];if (items.length === 0) {await div(`<div class="p-4 bg-white">No branches</div>`);return;}const branchSelected = await arg(`Search for branches`,items.map((b) => ({name: b.name,description: b.protected ? "Protected" : undefined,value: b,})));const action = await arg("Select an action to perform", [{name: `Copy name`,description: `Copy name to clipboard for reference elswhere (eg. in issue)`,value: "name",},{name: `Rename`,value: "rename",},]);switch (action) {case "name":await copy(branchSelected.name);exit();case "rename":const newName = await arg(`What should the new name be? (was ${branchSelected.name})`);await octokit.request(`POST /repos/{owner}/{repo}/branches/{branch}/rename`,{owner: repo.owner.login,repo: repo.name,branch: branchSelected.name,new_name: newName,});break;}notify(`${action} successful`);await getAllBranches(repo);}const issueHandler = createIssuePullHandler(`issue`);const pullHandler = createIssuePullHandler(`pull`);function getTabHandler(getter: () => Promise<OptionType<RawRepositoryType>[]>) {return async function handler() {const repos = await getter();const repoSelected = await arg(`Hello ${login}. Search for a repo`, repos);if (repos.length === 0) {await div(`<div class="p-4 bg-white">No repos</div>`);await handler();}const action = await arg("Select an action to perform",[...universalOptions,repoSelected.open_issues_count > 0 && {name: "Recent issues",description: "List top 10 most recent open issues",value: "issues",},{name: "Pull Requests",description: "List top 10 most recent PRs",value: "prs",},{name: "Branches",description: "List branches",value: "branches",},].filter(Boolean));switch (action) {case "browse":await browse(repoSelected.html_url);exit();case "copy":await copy(repoSelected.html_url);exit();case "issues":await issueHandler(repoSelected);break;case "prs":await pullHandler(repoSelected);break;case "branches":await getAllBranches(repoSelected);break;}notify(`${action} successful`);await handler();};}const recentTab = getTabHandler(fetchRecentRepos);onTab("Recent", recentTab);onTab("Owner", getTabHandler(fetchOwnerRepos));onTab("All", getTabHandler(fetchAllRepos));await recentTab();
// Name: Change case of selected text// Description: Choose a transformation method (eg. camelCase) to transform the selected text in-place (Or some text input)// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const changeCase = await npm("change-case");let text = await getSelectedText();if (text.length <= 1) {text = await arg("No text selected. What text should be transformed?");}const method = await arg("What transformation would you like to apply to your text?",[{name: "Camel",description: `test string -> testString`,value: "camelCase",},{name: "Capital",description: `test string -> Test String`,value: "capitalCase",},{name: "Constant",description: `test string -> TEST_STRING`,value: "constantCase",},{name: "Dot",description: `test string -> test.string`,value: "dotCase",},{name: "Header",description: `test string -> Test-String`,value: "headerCase",},{name: "Lower Case",description: `test string -> test string`,value: "noCase",},{name: "Slugify (dashes)",description: `test string -> test-string`,value: "paramCase",},{name: "Pascal",description: `test string -> TestString`,value: "pascalCase",},{name: "Path",description: `test string -> test/string`,value: "pathCase",},{name: "Sentence",description: `test string -> Test string`,value: "sentenceCase",},{name: "Snake (underscore)",description: `test string -> test_string`,value: "snakeCase",},]);if (!method in changeCase) {notify("The selected method '${method}' is not available");} else {const transformedText = changeCase[method](text);await Promise.all([setSelectedText(transformedText), copy(transformedText)]);notify(`The text was pasted and copied to the clipboard!`);}
// Name: Get Tabler Icons// Description: Copies a Tabler Icon from tabler open-sourced repository.// Author: Lucas Vogel// Twitter: @soyvogelinoimport "@johnlindquist/kit";const iconsPath = "https://tabler-icons.io/icons.json";let files = [];try {let response = await get(iconsPath);files = response.data;} catch (error) {notify(error);}const titleCase = (s) =>s.toLowerCase().split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");const options = files.map((file, idx) => {return {name: titleCase(file.n),description: file.t.split(" ").join(", "),value: file,preview: `<table class="w-full"><tr><td class="p-8"><div className="m-8">${file.s}</div></td><td class="p-8"><div>${file.s.replaceAll('"24"', '"100"')}</div></td><tr><td class="p-8 bg-black text-white"><div className="m-8">${file.s}</div></td><td class="p-8 bg-black text-white"><div>${file.s.replaceAll('"24"', '"100"')}</div></td></tr></tr></table>`,};});const icon = await arg("Search for an icon:", options);try {await copy(icon.s);notify(`Copied ${titleCase(file.n)} icon to clipboard`);} catch (error) {notify(error);}
// Name: Unsplash// Description: Search unsplash for the perfect image to download// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const { createApi } = await npm("unsplash-js");const accessKey = await env("UNSPLASH_ACCESS_KEY","Enter your unsplash Access Key");const downloadPath = await env("UNSPLASH_DOWNLOAD_PATH","Enter the path where to download the images");const headers = {"User-Agent": "scriptkit/unsplash",};const api = createApi({accessKey,});const query = await arg("What do you want to search");const photosRes = await api.search.getPhotos({query,page: 1,perPage: 10,});if (photosRes.errors) {console.log("error occurred: ", photosRes.errors[0]);} else {const { results } = photosRes.response;const options = results.map((photo) => ({name: photo.description || photo.alt_description || "Untitled",description: `${photo.user.name} โ ${photo.width} x ${photo.height} โ ${photo.likes} Likes`,preview: `<style>.image { object-fit: contain; height: 80vh; }</style><img class="w-screen image" src="${photo.urls.regular}" />`,value: photo,}));const selectedPhoto = await arg("Select a photo to copy", options);const buffer = await download(selectedPhoto.urls.raw, headers);const filePath = `${downloadPath}/unsplash-image-${selectedPhoto.id}.png`;await writeFile(filePath, buffer);notify(`Successfully downloaded file: ${filePath}`);}
// Name: Todoist// Description: Create and browse your Todoist Tasks// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const { TodoistApi } = await npm("@doist/todoist-api-typescript");const { formatRelative } = await npm("date-fns");interface ProjectType {id: string;name: string;}interface TaskType {id: string;content: string;order: number;due?: {datetime: string;};}const today = new Date();const fromNow = (date: string) => formatRelative(new Date(date), today);function sortTaskByDueDateOrOrder(a: TaskType, b: TaskType) {const dateA = a.due?.datetime? new Date(a.due?.datetime).getTime(): Infinity;const dateB = b.due?.datetime? new Date(b.due?.datetime).getTime(): Infinity;return dateA - dateB || a.order - b.order;}const apiKey = await env("TODOIST_API_KEY","Please enter your Todoist API key");const api = new TodoistApi(apiKey);const allProjects = (await api.getProjects()) as ProjectType[];const projectTabs = [{ id: undefined, name: "All" }, ...allProjects].reduce((acc, project) => ({...acc,[project.name]: async function (tasks?: TaskType[]) {if (!tasks) {tasks = (await api.getTasks(project.id ? { project_id: project.id } : undefined)) as TaskType[];}const options = tasks.sort(sortTaskByDueDateOrOrder).map((task) => ({name: task.content,description: task.due?.datetime? fromNow(task.due?.datetime): undefined,value: task.id,}));const taskIdToComplete = await arg(`Search for a task in "${project.name}"`,options);await api.closeTask(taskIdToComplete);await projectTabs[project.name](tasks.filter((t) => t.id !== taskIdToComplete));},}),{});const newTask = async () => {const content = await arg("What is your task about?");const dueString = await arg("Enter the due date or leave empty");const projectId = await arg("In which project? (leave empty for Inbox)",allProjects.map((project) => ({name: project.name,value: project.id,})));try {await api.addTask({content,dueString,dueLang: "en",project_id: projectId,});notify(`The task "${content}" was added!`);} catch (err) {await div(`<p class="m-4 px-6 py-2 rounded bg-white border" style="border-color: red;">๐ด The task could not be added: <code class="inline">${err.responseData}</code></p>`);}await newTask();};onTab("New", newTask);Object.keys(projectTabs).forEach((projectName) => {onTab(projectName, projectTabs[projectName]);});
// Name: Search for YouTube Videos// Description: Prompts for a search string and lists all youtube videos found (with preview)// Author: Vogelino// Twitter: @soyvogelino// Shortcut: opt yimport "@johnlindquist/kit";const apiKey = await env("YOUTUBE_API_KEY");const previewAudioOn =(await env("YOUTUBE_VIDEO_SEARCH_PREVIEW_AUDIO_ON","Would you like the video preview to run with sound? [y/n]")) === "y";const previewAutoplayOn =(await env("YOUTUBE_VIDEO_SEARCH_PREVIEW_AUTOPLAY_ON","Would you like the video preview to play automatically? [y/n]")) === "y";const searchYoutubeVideos = async (searchString) => {if (!searchString) throw new Error(`No search string entered`);const ytUrl = new URL(`https://www.googleapis.com/youtube/v3/search`);ytUrl.searchParams.set("key", apiKey);ytUrl.searchParams.set("q", searchString);ytUrl.searchParams.set("part", "snippet");const { data } = await get(ytUrl.toString());return data.items;};try {let searchString = await arg("Enter your search");let videos = await searchYoutubeVideos(searchString);while (videos.length === 0) {searchString = await arg("No results. Enter a new search");videos = await searchYoutubeVideos(searchString);}const videoId = await arg("Select a video to open",videos.map(({ snippet, id }) => ({name: snippet.title,description:snippet.description.length > 50? `${snippet.description.slice(0, 50)}...`: snippet.description,preview: () => `<div style="padding: 16px"><table style="margin-bottom: 8px;"><tr><td width="96px"><imgsrc="${snippet.thumbnails.default.url}"width="80px"style="float: left; margin-right: 16px"/></td><td><h3>${snippet.title}</h3></td></tr></table><iframesrc="http://www.youtube.com/embed/${id.videoId}?autoplay=${previewAudioOn ? 1 : 0}"width="560"height="315"frameborder="0"${previewAutoplayOn && "autoplay"}style="aspect-ratio: 16 / 9; width: 100%;"></iframe></div>`,value: id.videoId,})));browse(`https://www.youtube.com/watch?v=${videoId}`);} catch (err) {console.log(err);notify(err);}
// Name: New Jitsi Meeting URL// Description: Copies a random Jitsi Meeting URL to clipboard and offers to open it// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const { hri } = await npm("human-readable-ids");const newMeetingSlug = hri.random();const newMeetingUrl = `https://meet.jit.si/${newMeetingSlug}`;await copy(newMeetingUrl);const shouldBrowse = await arg("Do you want to open the link?", [{ name: "No", description: "๐พ - just copy", value: false },{ name: "Yes", description: "๐พ + โ๏ธ - copy and open", value: true },]);shouldBrowse && browse(newMeetingUrl);
// Name: NTS Live// Description: Stream NTS Live Channel 1 or 2// Author: Vogelino// Twitter: @soyvogelino// Shortcut: cmd 'import "@johnlindquist/kit";const PLAYER_HEIGHT = 56;const PLAYER_WIDTH = 170;const getLogoSVG = (color) => `<svg width="32px" height="32px" style="fill:${color};" viewBox="0 0 26 26"><path d="M22.7 6.9L22.3 9h-1.5l.5-2c.1-.6.1-1.1-.6-1.1s-1 .5-1.1 1.1l-.4 1.7c-.1.5-.1 1 0 1.5l1.4 4.1c.2.6.3 1.3.1 2l-.6 2.6c-.4 1.5-1.5 2.4-2.9 2.4-1.6 0-2.3-.7-1.9-2.4l.5-2.2h1.5l-.5 2.1c-.2.8 0 1.2.7 1.2.6 0 1-.5 1.2-1.2l.5-2.3c.1-.5.1-1.1-.1-1.6l-1.3-3.8c-.2-.7-.3-1.2-.2-2.1l.4-2c.4-1.6 1.4-2.4 2.9-2.4 1.7 0 2.2.8 1.8 2.3zM11.2 21.1L14.6 6H13l.3-1.3h4.8L17.8 6h-1.7l-3.4 15.1h-1.5zm-4.5 0L8.1 6.6 4.8 21.1H3.5L7.2 4.8h2.2L8 18.7l3.2-14h1.3L8.8 21.1H6.7zM0 26h26V0H0v26z"></path></svg>`;const getCloseIconSVG = (color) => `<svg id="x" class="ml-1" width="32px" height="32px" viewBox="0 0 24 24" fill="${color}" class="pointer-event-none"><path id="x" fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd"/></svg>`;const btnCommonClasses = `px-3 text-xl cursor-default`;const getJavascriptCode = () => `const channels = {one: "https://stream-relay-geo.ntslive.net/stream2?client=NTSWebApp&t=1663096772944",two: "https://stream-relay-geo.ntslive.net/stream?client=NTSWebApp&t=1663095867072",};let currentChannel = channels.one;function setChannel(channelKey) {const audio = document.getElementById('audioPlayer');const source = document.getElementById('audioSource');currentChannel = channels[channelKey];source.src = currentChannel;audio.load();audio.play();}function setActiveClasses(btnEl) {btnEl.classList.remove("text-white")btnEl.classList.add("bg-white")btnEl.classList.add("text-black")}function setInactiveClasses(btnEl) {btnEl.classList.add("text-white")btnEl.classList.remove("bg-white")btnEl.classList.remove("text-black")}const btnOne = document.getElementById('channelOneButton');btnOne.addEventListener('click', () => {setActiveClasses(btnOne);setInactiveClasses(btnTwo);setChannel("one");}, false);const btnTwo = document.getElementById('channelTwoButton');btnTwo.addEventListener('click', () => {setActiveClasses(btnTwo);setInactiveClasses(btnOne);setChannel("two");}, false);setActiveClasses(btnOne);setInactiveClasses(btnTwo);setChannel("one");`;const wgt = await widget(`<div class="bg-black p-4 text-white"><table><tr><td width="32px" class="pr-2">${getLogoSVG("white")}</td><td id="channelOneButton" class="${btnCommonClasses}">1</td><td id="channelTwoButton" class="${btnCommonClasses}">2</td><td id="x">${getCloseIconSVG("white")}</td></tr></table><audio id="audioPlayer" class="hidden" controls controlslist="nofullscreen nodownload noremoteplayback noplaybackrate"><source id="audioSource"></audio></div><script type="text/javascript">${getJavascriptCode()}</script>`,{ alwaysOnTop: true, state: { channelKey: "one" } });wgt.setSize(PLAYER_WIDTH, PLAYER_HEIGHT);wgt.onClick((event) => event.targetId === "x" && wgt.close());wgt.onResized(() => wgt.setSize(PLAYER_WIDTH, PLAYER_HEIGHT));
// Name: Download YouTube Video// Description: Download a video from a YouTube URL as .mp4// Author: Vogelino// Twitter: @soyvogelinoimport "@johnlindquist/kit";const youtubeDlExec = await npm("youtube-dl-exec");const slugify = await npm("slugify");const apiKey = await env("YOUTUBE_API_KEY");// Show feedback as HTML (Adds padding and some feedback styles)const showFeedback = async (message) => {await div(`<style type="text/css">.container { position: relative; padding-right: 232px !important; }.error { border: 1px solid red; color: red; background: rgba(255,0,0,.1); }.success { border: 1px solid green; color: darkgreen; background: rgba(0,255,0,.1); }.default { border: 1px solid gray; color: black; background: rgba(0,0,0,.05); }</style><main class="p-8">${message}</main>`);return message;};// Returns the SVG markup of an animated loading spinnerconst getLoadingSpinner = () => `<svgstyle="margin: 0 8px 0 0; display: inline-block; shape-rendering: auto"width="30px"height="30px"viewBox="0 0 100 100"preserveAspectRatio="xMidYMid"><defs><clipPath id="progress-4hqxcfiwb2u-cp" x="0" y="0" width="100" height="100"><rect x="0" y="0" width="0" height="100"><animateattributeName="width"repeatCount="indefinite"dur="1s"values="0;100;100"keyTimes="0;0.5;1"></animate><animateattributeName="x"repeatCount="indefinite"dur="1s"values="0;0;100"keyTimes="0;0.5;1"></animate></rect></clipPath></defs><pathfill="none"stroke="rgba(0,0,0,.2)"stroke-width="2.79"d="M18 36.895L81.99999999999999 36.895A13.104999999999999 13.104999999999999 0 0 1 95.10499999999999 50L95.10499999999999 50A13.104999999999999 13.104999999999999 0 0 1 81.99999999999999 63.105L18 63.105A13.104999999999999 13.104999999999999 0 0 1 4.895000000000003 50L4.895000000000003 50A13.104999999999999 13.104999999999999 0 0 1 18 36.895 Z"></path><pathfill="rgba(0,0,0,.8)"clip-path="url(#progress-4hqxcfiwb2u-cp)"d="M18 40.99L82 40.99A9.009999999999998 9.009999999999998 0 0 1 91.00999999999999 50L91.00999999999999 50A9.009999999999998 9.009999999999998 0 0 1 82 59.01L18 59.01A9.009999999999998 9.009999999999998 0 0 1 8.990000000000004 50L8.990000000000004 50A9.009999999999998 9.009999999999998 0 0 1 18 40.99 Z"></path></svg>`;// Returns a small HTML structure showing basic information about the video currently downloadedconst getVideoTemplate = (title, metadata) => `<h1 class="w-full truncate">${title}</h1><table className="container"><tr><td width="${(metadata.thumbnails?.default?.width || 0) + 32}" className="pr-4 align-top"><imgsrc="${metadata.thumbnails?.default?.url}"width="${metadata.thumbnails?.default?.width}"height="${metadata.thumbnails?.default?.height}"/></td><td class="align-top"><div><strong class="block">Destination</strong><span>${metadata.path}</span></div><div><strong>Language</strong><span>${metadata.defaultAudioLanguage}</span></div><div><strong>Channel</strong><span>${metadata.channelTitle}</span></div></td></tr></table>`;// Retruns basic information about the youtube video (For the feedback and the file name)const getVideoMetadata = (url) =>new Promise((resolve, reject) => {const urlObj = new URL(url);const id = urlObj.searchParams.get("v");if (!id) return reject(`Video ID not present in the url`);const ytUrl = new URL(`https://www.googleapis.com/youtube/v3/videos`);ytUrl.searchParams.set("key", apiKey);ytUrl.searchParams.set("id", id);ytUrl.searchParams.set("part", "snippet");console.log(ytUrl.toString());get(ytUrl.toString()).then((response) => response.data).then((data) => data.items[0].snippet).then(resolve);});// We save the metadata outside the try catch so it's available in the catchlet fullMetadata = {};try {const videoSrc = await arg("Video url:");const videoMetadata = await getVideoMetadata(videoSrc);const videoPath = "Downloads";const videoName = slugify(videoMetadata.title.slice(0, 50).toLowerCase());const fileName = videoName !== "" ? videoName : videoSrc;const newPath = home(videoPath, path.basename(fileName) + ".mp4");fullMetadata = { ...videoMetadata, path: `~/${videoPath}/${fileName}.mp4` };// We display the loading statevoid showFeedback(`<div class="px-6 py-4 rounded default">${getVideoTemplate(`${getLoadingSpinner()} Downloading "${fullMetadata.title}"`,fullMetadata)}</div>`);// We download the videoconst res = await youtubeDlExec(videoSrc, { output: newPath });console.log(res);// If all went well, we can show a success messageshowFeedback(`<div class="px-6 py-4 rounded success">${getVideoTemplate(`โ Successfully downloaded "${fullMetadata.title}"`,fullMetadata)}</div>`);await wait(1000);// After a second, we offer the user the choice of what to do nextconst nextStep = await arg("What would you like to do with this file?", [{name: "Show in finder โ๏ธ",description: `Open ~/${videoPath}`,value: "locate",},{name: "Open video ๐ฅ",description: `View video in default player`,value: `view`,},]);// We check for the user's choice in and open either the file or the locationif (nextStep === "locate") {exec(`open --reveal ${newPath}`);} else if (nextStep === "view") {exec(`open ${newPath}`);}// In case something went wrong, we show the error} catch (err) {console.log(err);await showFeedback(`<div class="px-6 py-4 rounded error"><p class="px-6 py-4 mb-4 rounded error">๐ด Error ${err}</p>${getVideoTemplate(`๐ด Error downloading "${fullMetadata?.title}"`,fullMetadata || {})}</div>`);}