// Menu: Currency Converter using google// Author: Jakub Olek// Twitter: @JakubOlek/** @type {import("@johnlindquist/kit")} */const from = await arg("from", ["pln", "usd", "eur"]);const to = await arg("to", ["usd", "pln", "eur"]);const query = await arg("amount");const value = await scrapeSelector(`https://google.com/search?q=${encodeURIComponent(query + " " + from + " " + to)}`,"span[data-value]");div(query + " " + from + " = " + value + " " + to, "p-4");
// Menu: RCKIK mobile// Description: Show filtered plan of mobile RCKIK busses// Author: Jakub Olek// Twitter: @JakubOlek/** @type {import("@johnlindquist/kit")} */function transform(node) {// Edit that to filter locations that you're interested in// has to be inside transform function - as it's being serialized and passed to browserconst filter = "Poznań";const columns = [...node.querySelectorAll("td")];if (columns[3].innerHTML.startsWith(filter)) {return (columns[0].querySelector("a").innerHTML +" " +columns[3].innerHTML +" " +columns[2].innerHTML);}}const pagination = await scrapeSelector("https://www.rckik.poznan.pl/najblizsze-wyjazdy?page=1",".pagination li");const numberOfPages = pagination.length;let t = [];for (let i = 1; i <= numberOfPages; i += 1) {const result = await scrapeSelector(`https://www.rckik.poznan.pl/najblizsze-wyjazdy?page=${i}`,"#calendarTable tr:not(.header):not(.canceled)",transform);t = t.concat(result);}div(`<ul>${t.filter(Boolean).map((date) => `<li>${date}</li>`).join("")}</ul>`,"p-4");
// Menu: Weather by yr.no// Author: Jakub Olek// Twitter: @JakubOlek/** @type {import("@johnlindquist/kit")} *//*** 1. Open https://www.yr.no/* 2. Find your location* 3. Copy what comes AFTER https://www.yr.no/en/forecast/daily-table/ to location const*/const location = "5-1233000/Poland/Poznan";div(`<iframe src="https://www.yr.no/en/print/forecast/${location}#toolbar=0" height=800 width=600/>`,"p-2");
// Menu: Record Screen// Description: Start QuickTime player and open screen recorder// Author: Jakub Olek// Twitter: @JakubOlekawait run("app-launcher", "QuickTime Player");await keystroke("command control n");
// Menu: Ping// Description: Ping destination and show line graph of latest values// Author: Jakub Olek// Twitter: @JakubOlekconst jsdom = await npm("jsdom");await npm("canvas");const Chart = await npm("chart.js");const { JSDOM } = jsdom;// Edit the list to suit your needsconst destination = await arg("ping", ["8.8.8.8", "google.com"]);// How many entries should the chart showconst entries = 20;const command = `ping ${destination}`;const child = exec(command, { async: true });const dom = new JSDOM(`<!DOCTYPE html><canvas id="bar-chart" width="800" height="450"></canvas>`);global.window = dom.window;Chart.defaults.color = "white";Chart.defaults.font = { size: 24, weight: "bold" };const labels = new Array(entries).fill().map((_, i) => i).reverse();let output = [];const chartData = {labels: labels,datasets: [{label: `${command}: - ms`,backgroundColor: "rgb(255, 99, 132)",borderColor: "rgb(255, 99, 132)",data: output,},],};const chart = new Chart(dom.window.document.getElementById("bar-chart"), {type: "line",data: chartData,options: {animation: false,tooltips: { enabled: false },hover: { mode: null },},});let firstLine = true;child.stdout.on("data", function (data) {if (!firstLine) {const value = +data.replace(/.*time=(.*)ms/, "$1");output.push(value);if (output.length > entries) {output = output.slice(1);}chartData.datasets[0].label = `${command}: ${value}ms`;chartData.datasets[0].data = output;chart.update();div(`<img src="${chart.toBase64Image("image/jpeg", 1)}"/>`, "p-4");} else {firstLine = false;}});
// Menu: Open Jira ticket in browser// Description: Parses a valid ticket number from selection and opens it in browser// Author: Jakub Olek// Twitter: @JakubOlekconst jiraDomain = await env("JIRA_DOMAIN");const text = await getSelectedText();const jiraTicket = text.match(/([A-Z]{2,5}-[0-9]+)/);if (jiraTicket) {focusTab(`${jiraDomain}/browse/${jiraTicket[0]}`);}
// Menu: GitLab - next MR// Description: Open next MR that I have not approved// Author: Jakub Olek// Twitter: @JakubOlek// Shortcut: ctrl opt \const { request, gql, GraphQLClient } = await npm("graphql-request");const dayjs = await npm("dayjs");import relativeTime from "dayjs/plugin/relativeTime.js";dayjs.extend(relativeTime);const domain = await env("GITLAB_DOMAIN");const token = await env("GITLAB_TOKEN");const username = await env("GITLAB_USERNAME");const jiraDomain = await env("JIRA_DOMAIN");const requiredApprovals = Number(await env("GITLAB_REQUIRED_APPROVALS"));const debug = false;function log(...args) {if (debug) {console.log(...args);}}const graphQLClient = new GraphQLClient(domain + "/api/graphql", {headers: {"PRIVATE-TOKEN": token,},});const projects = gql`query($name: String!) {projects(search: $name, membership: true) {nodes {nameWithNamespacefullPath}}}`;if (!env.GITLAB_PROJECT_PATH) {const fullPath = await arg("Search project", async (input) => {return (await graphQLClient.request(projects, { name: input })).projects.nodes.map((project) => ({name: project.nameWithNamespace,description: project.fullPath,value: project.fullPath,}));});await cli("set-env-var", "GITLAB_PROJECT_PATH", fullPath);}const queryMrs = gql`query($projectPath: ID!) {project(fullPath: $projectPath) {mergeRequests(state: opened, sort: UPDATED_DESC) {nodes {titlewebUrliiddraftdescriptioncreatedAtapprovedBy {nodes {nameusername}}author {nameusernameavatarUrl}}}}}`;const query = gql`query($iid: String!, $projectPath: ID!) {project(fullPath: $projectPath) {mergeRequest(iid: $iid) {commitsWithoutMergeCommits(first: 1) {nodes {authoredDate}}headPipeline {status}notes {nodes {updatedAtauthor {username}}}}}}`;let nextMR;const myMrs = [];const drafts = [];const awaitingReview = [];const alreadyCommented = [];const haveAuthorCommented = [];const haveOthersCommented = [];const haveFailingPipeline = [];const alreadyApprovedByMe = [];const alreadyApprovedByOthers = [];const {project: {mergeRequests: { nodes: mergeRequests },},} = await graphQLClient.request(queryMrs, {projectPath: env.GITLAB_PROJECT_PATH,});arg("Processing...");log("Show list", flag.showList);log("Checking", mergeRequests.length, "MRs");for (let mr of mergeRequests) {log("Checking MR", mr.title, `(${mr.author.username})`);const approvedBy = mr.approvedBy.nodes.map((node) => node.username);if (mr.author.username === username) {log("^ This is my MR");myMrs.push(mr);continue;}if (mr.draft) {drafts.push(mr);log("^ This is a draft");continue;}if (approvedBy.includes(username)) {log("^ Approved by me");alreadyApprovedByMe.push(mr);continue;} else {if (approvedBy.length >= requiredApprovals) {log("^ Approved by others");alreadyApprovedByOthers.push(mr);continue;}const {project: { mergeRequest },} = await graphQLClient.request(query, {iid: mr.iid,projectPath: env.GITLAB_PROJECT_PATH,});const pipelineStatus = mergeRequest.headPipeline.status;if (pipelineStatus !== "SUCCESS") {log("^ Failed pipeline");haveFailingPipeline.push(mr);continue;}const comments = mergeRequest.notes.nodes;const anyLatestComment = comments[0];const myLatestComment = comments.find((comment) => comment.author.username === username);const authorLatestComment = comments.find((comment) => comment.author.username === mr.author.username);if (myLatestComment) {const latestCommitTime = dayjs(mergeRequest.commitsWithoutMergeCommits.nodes[0].authoredDate);const myLatestCommentTime = dayjs(myLatestComment.updatedAt);if (latestCommitTime.isBefore(myLatestCommentTime)) {log("^ awaits new commits after my comments");alreadyCommented.push(mr);continue;}if (authorLatestComment) {const authorLatestCommentTime = dayjs(authorLatestComment.updatedAt);if (authorLatestCommentTime.isAfter(myLatestComment.updatedAt)) {log("^ have some comments by the MR author after my comment");haveAuthorCommented.push(mr);continue;}}if (anyLatestComment) {const latestCommentTime = dayjs(anyLatestComment.updatedAt);if (latestCommentTime.isAfter(myLatestComment.updatedAt)) {log("^ have some comments by other after my comment");haveOthersCommented.push(mr);continue;}}}if (!flag.showList) {nextMR = mr;break;} else {awaitingReview.push(mr);}}}function createJiraLinks(text) {return text.replace(/[A-Z]{1,5}-[0-9]*/g,(ticketNumber) => `[${ticketNumber}](${jiraDomain}}/browse/${ticketNumber})`);}function getName(mr) {if (mr.author.username === username) {return `${!mr.draft && mr.approvedBy.nodes.length < 2 ? "!A " : ""}${mr.title}`;}return mr.title;}function getChoices(mrs, description) {return mrs.map((mr) => ({name: getName(mr),value: mr.webUrl,description: description,img: mr.author.avatarUrl.includes("http")? mr.author.avatarUrl: domain + mr.author.avatarUrl,preview: md(`# ${createJiraLinks(mr.title)}## Created ${dayjs(mr.createdAt).fromNow()} by ${mr.author.name}## ${description}## Approved by${mr.approvedBy.nodes.length? mr.approvedBy.nodes.map((user) => `* ${user.name}`).join(""): "- nobody"}${createJiraLinks(mr.description.replace(/\/uploads\//g,domain + "/uploads/" + env.GITLAB_PROJECT_PATH + "/"))}`),}));}if (nextMR) {await focusTab(nextMR.webUrl);} else {const choices = [...getChoices(awaitingReview, "Awaiting Review"),...getChoices(haveAuthorCommented, "Author have comments after you"),...getChoices(haveOthersCommented, "Someone have comments after you"),...getChoices(myMrs, "My merge request"),...getChoices(haveFailingPipeline, "Failing Pipeline"),...getChoices(alreadyCommented, "You have commented on this"),...getChoices(alreadyApprovedByOthers, "Already approved by others"),...getChoices(alreadyApprovedByMe, "Already approved by you"),...getChoices(drafts, "Draft"),];if (choices.length) {const mr = await arg("Open MR:", choices);if (mr) {focusTab(mr);}}}
// Menu: Conventional comment// Description: Comments that are easy to grok and grep// Author: Jakub Olek// Twitter: @JakubOlek// Shortcut: opt 0// Based on: https://hemdan.hashnode.dev/conventional-commentsconst type = await arg("Label", [{name: "👏 praise",value: "**👏 praise**: ",description:"Praises highlight something positive. Try to leave at least one of these comments per review (if it exists :^)",},{name: "🤓 nitpick",value: "**🤓 nitpick**: ",description:"Nitpicks are small, trivial, but necessary changes. Distinguishing nitpick comments significantly helps direct the reader's attention to comments requiring more involvement.",},{name: "🎯 suggestion",value: "**🎯 suggestion**: ",description:"Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously.",},{name: "🔨 issue",value: "**🔨 issue**: ",description:"Issues represent user-facing problems. If possible, it's great to follow this kind of comment with a suggestion.",},{name: "❔ question",value: "**❔ question**: ",description:"Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution.",},{name: "💭 thought",value: "**💭 thought**: ",description:"Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities.",},{name: "💣 chore",value: "**💣 chore**: ",description:"Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common processes. Try to leave a link to the process described so that the reader knows how to resolve the chore.",},]);setSelectedText(type);
// Menu: Snippets// Description: Snippets collection// Author: Jakub Olek// Twitter: @JakubOlek// Shortcut: opt -const { setSelectedText } = await kit("text");function capitalizeFirstLetter(string) {return string.charAt(0).toUpperCase() + string.slice(1);}const snippetMap = {useState: {args: [() => arg("variable name"), () => arg("variable value")],template: (name, value) =>`const [${name}, set${capitalizeFirstLetter(name)}] = useState(${value})`,},name: "Jakub Olek",date: new Date().toLocaleDateString("en-GB", {year: "numeric",month: "long",day: "numeric",}),test: {args: [() => arg("should")],template: (testName) => `test("should ${testName}", function() {})`,},component: {args: [() => arg("component name")],template: (componentName) => `function ${capitalizeFirstLetter(componentName)}() {return}`,},};const snippetName = await arg("Snippet", Object.keys(snippetMap));let result = snippetMap[snippetName];const { args, template } = result;if (template) {const variables = [];if (args) {for (let i = 0; i < args.length; i++) {const variable = args[i];if (typeof variable !== "string") {variables.push(await variable());}}}setSelectedText(template(...variables));} else {setSelectedText(result);}
// Menu: Calculator// Author: Jakub Olek// Twitter: @JakubOlek// Shortcut: opt =const calcDb = await db("calc", { history: [] });function createResult(calculationResult, input) {return {name: calculationResult,description: input,value: { calculationResult, input },};}const { calculationResult, input, ...rest } = await arg("0", async (input) => {const choices = [];if (input) {let { stdout } = exec(`bc <<<"${input.replace(/\,/g, ".")}" -l`,{ silent: true } // Avoids printing the errors in the terminal);if (stdout) {choices.push(createResult(stdout.replace(/^\./, "0.").trim(), input));return choices.concat(calcDb.history);}}});if (calculationResult) {const history = calcDb.history;history.unshift(createResult(calculationResult, input));calcDb.history = history.filter(({ description }, index) => index === 0 || description !== input).slice(0, 10);await calcDb.write();}copy(calculationResult);