Z Zeleznick

Z Zeleznick

<details> <summary> <code>lib/image-grid.js</code> </summary>
// lib/image-grid.js
const DEFAULT_LIMIT = 10000; // 1000; // 100;
const DEBUG = { ENABLED: false };
const debug = (...args) => DEBUG.ENABLED && console.log(...args)
const info = (...args) => console.log(...args)
export const enableDebugMode = () => { DEBUG.ENABLED = true }
const getImages = (filepath, maxdepth) => {
// NOTE: options to use "-ctime -90d" / "-atime -90d" to filter more results
const findCommand = `find -E ${filepath} -iregex '.*\.(jpg|jpeg|png|gif)' -maxdepth ${maxdepth}`
const findSortedCommand = `${findCommand} -print0 | xargs -0 ls -at`
debug("findSortedCommand", findSortedCommand)
return exec(findSortedCommand, { silent: true }).toString().split("\n").filter(v => v)
}
const buildImageModal = (payload) => {
let {file} = payload;
const img = `<img src="${file}">`
return `<div class="imgContainer">${img}</div>`
}
const injectCss = (html) => {
// our tailwind build doesn't include grid css
// we add some custom styles as well
const css = `
/* Mimic tailwind grid css */
.grid {display:grid}
.grid-cols-3 {grid-template-columns: repeat(3, minmax(0, 1fr))}
.grid-cols-4 {grid-template-columns: repeat(4, minmax(0, 1fr))}
.grid-cols-5 {grid-template-columns: repeat(5, minmax(0, 1fr))}
/* custom css to center images in grid */
.grid div {place-items: center; padding: clamp(1px, 4%, 25px);}
.imgContainer {display: flex;}
`
const style = `<style type="text/css">${css}</style>`
return `${style}${html}`
}
const buildPage = (imageObjects, limit = DEFAULT_LIMIT) => {
const subset = imageObjects
.slice(0, limit)
.map(file => { return { file } })
const columns = subset.length > 32 ? (subset.length > 64 ? 5 : 4) : 3
const modals = subset.map(buildImageModal).join('\n')
const html = `<div class="grid grid-cols-${columns} pt-1 m-1">${modals}</div>`
const page = injectCss(html)
debug(page);
info('buildPage: Done')
return page
}
export const buildImagesPanel = async (filepath, maxdepth, limit) => {
const images = getImages(filepath, maxdepth);
info(`Found ${images.length} images`);
await arg({
input: " ",
}, buildPage(images, limit));
}
</details> <details> <summary> <code>view-desktop.js</code> </summary>
// Menu: View Desktop
// Description: View Desktop Attachments
// Author: Zach Zeleznick
// Twitter: @zzxiv
// Shortcut: cmd shift d
const {buildImagesPanel} = await lib("image-grid")
const filepath = "~/Desktop"
const depth = "3"
await buildImagesPanel(filepath, depth)
</details> <details> <summary> <code>view-attachments.js</code> </summary>
// Menu: View Attachment
// Description: View iMessage Attachments
// Author: Zach Zeleznick
// Twitter: @zzxiv
// Shortcut: cmd shift l
const {buildImagesPanel} = await lib("image-grid")
// NOTE: Need to grant Kit app full disk access in Security and Privacy or find will return 0 results
const filepath = "~/Library/Messages/Attachments"
const depth = "4"
await buildImagesPanel(filepath, depth)
</details> <details> <summary> <code>view-downloads.js</code> </summary>
// Menu: View Download
// Description: View Download Attachments
// Author: Zach Zeleznick
// Twitter: @zzxiv
// Shortcut: cmd shift 0
const {buildImagesPanel, enableDebugMode} = await lib("image-grid")
const filepath = "~/Downloads"
const depth = "2"
const limit = 42
enableDebugMode()
await buildImagesPanel(filepath, depth, limit)
</details> <table> <tr> <td><img width="863" alt="Native View" src="https://user-images.githubusercontent.com/5779832/116947788-7a972480-ac32-11eb-9e22-10dfb1fa3dff.png"></td> <td><img width="490" alt="Sorting Options" src="https://user-images.githubusercontent.com/5779832/116947778-779c3400-ac32-11eb-9378-4bb8c97012d6.png"></td> </tr> <tr> <td>Example search for `png` files on my Desktop</td> <td>Sorting options</td> </tr> </table>

<details> <summary>Click for code </summary>
// Menu: Kit Scripts
// Description: View + Copy Scripts
// Author: Zach Zeleznick
// Twitter: @zzxiv
const Prism = await npm('prismjs')
const scriptsDB = db("kit-scripts", { scripts: [] });
const scriptsRef = scriptsDB.get("scripts");
const owner = `eggheadio`
const repo = `scriptkit.app`
const branch = `main`
const author = `johnlindquist`
const treepath = `public/scripts/${author}`
const ref = `${branch}:${treepath}`
const githubURL = "https://api.github.com/graphql";
let token = env.GITHUB_ACCESS_TOKEN;
const config = {
headers: {
"Authorization": `Bearer ${token}`,
}
}
if (!token) {
const element = `
<div class="flex flex-col justify-center">
<div>
<a href="https://github.com/settings/tokens/new">Create a token</a> with "public_repo" enabled.
</div>
<br>
<div>
Then, copy + paste the token above or set <code>GITHUB_REPO_TOKEN</code> inside <code>~/.kenv/.env</code>
</div>
</div>`
token = await env("GITHUB_ACCESS_TOKEN", {
info: `Create and enter your personal access token`,
choices: element,
});
}
const repoTreeQuery = `
query {
repository(owner: "${owner}", name: "${repo}") {
object(expression: "${ref}") {
... on Tree {
entries {
name,
oid,
}
}
}
}
}`
const fetchTreeObjects = async () => {
let response;
try {
response = await post(githubURL,
{
query: repoTreeQuery
},
config
);
}
catch (err) {
console.warn("fetchTreeObjects failed:", err);
return
}
const graphqlResponse = response.data;
// console.log(repoTreeQuery, graphqlResponse);
const {
data: {
repository: {
object: {
entries
}
}
}
} = graphqlResponse;
return entries
}
const fetchScript = async (name) => {
const scriptUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${treepath}/${name}`;
// NOTE: https://scriptkit.app/scripts/${author}/${name} should also work by design
const response = await get(scriptUrl);
return response.data;
}
// adapted from kit/cli/info.js
const getByMarker = marker => text => {
const exp = new RegExp(`${marker}(.*)`);
const match = text.match(exp)
if (!match) return
return match[1].trim()
}
const extractMetadata = (text) => {
const mapping = {
menu: 'Menu:',
description: 'Description:',
author: 'Author:',
twitter: 'Twitter:',
}
return Object.entries(mapping)
.reduce((a, [k,v]) => {
return {...a, [k]: getByMarker(v)(text)}
}, {});
}
const loadScriptBundle = async (name, oid) => {
const entry = scriptsRef.find({ name });
const cached = entry.value();
if (cached) {
console.log(`Found cached for: ${name}`);
const localOid = cached.oid;
if (localOid === oid) {
console.log(`No remote changes for: ${name}`);
return cached
}
console.log(`Git object mismatch for: ${name} – local:${localOid} != remote:${oid}`);
}
console.log(`Fetching remote ${name}`);
const text = await fetchScript(name);
const metadata = extractMetadata(text);
console.log(`Fetched remote ${name} with metadata: ${JSON.stringify(metadata)}`);
const payload = { ...metadata, text, name, oid}
// TODO: should probably remove old files
// TODO: should clean up this function – doing too much
if (cached) {
entry.assign(payload).write()
} else {
scriptsRef.insert(payload).write();
}
return payload
}
// MARK: currently unused
const injectCustomClass = async () => {
// Load per suggestion on https://github.com/PrismJS/prism/issues/1171#issuecomment-470929808
// Source: https://github.com/PrismJS/prism/blob/master/plugins/custom-class/prism-custom-class.js
await npm('prismjs/plugins/custom-class/prism-custom-class')
// injects into Prism.plugins (e.g run 'console.log(Object.keys(Prism.plugins))' before + after)
Prism.plugins.customClass.add(({language, type, content}) => {
if (language === 'javascript') {
return 'overflow-scroll';
}
});
}
const buildCodeBlock = (code) => {
const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
return `<div class="h-full p-1 pt-2 pb-2 text-xs w-screen"><pre><code>${html}</code></pre></div>`
}
const smallTextify = (field) => {
return field ? `<div class="text-xs">${field}</div>` : ''
}
// NOTE: couldn't trigger the app.on('open-url') and would instead get the app in a bad state ...
// const buildUrl = (name) => `kit://${name.split('.')[0]}?url=https://${repo}/scripts/${author}/${name}`
const buildUrl = (name) => `https://${repo}/scripts/${author}/${name.split('.')[0]}`
const buildCodeModal = (payload) => {
let {name, text: code, description, author, twitter} = payload;
const block = buildCodeBlock(code)
const download = `<a class="group font-mono font-bold inline-flex" href="${buildUrl(name)}">Install</a>`
name = name ? `<div class="text-lg font-mono font-bold">${name.split('.')[0]}</div>` : ''
const row = `<div class="flex w-full justify-between">${name}${download}</div>`
const meta = [row].concat([description, author, twitter].map(smallTextify)).join('\n');
// ideally add some fancier styles like 'box-border border-4 bg-white' here
const metaStyle = "border-bottom: 2px solid rgba(0, 0, 0, .025)"
const header = `<div class="h-full p-3" style="${metaStyle}">${meta}</div>`
const style = "border: 2px solid rgba(0, 0, 0, .05); overflow: scroll;"
return `<div class="h-full w-full p-1 pb-2 mb-2 " style="${style}">${header}${block}</div>`
}
const injectCss = (html) => {
// see https://unpkg.com/prism-theme-night-owl@1.4.0/build/light.css
// source: https://github.com/SaraVieira/prism-theme-night-owl
const css = `code[class*=language-],pre[class*=language-]{color:#403f53;font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#fbfbfb}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#fbfbfb}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{color:#fff;background:#fbfbfb}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.prolog{color:#989fb1;font-style:italic}.token.punctuation{color:#994cc3}.namespace{color:#0c969b}.token.deleted{color:rgba(239,83,80,.56);font-style:italic}.token.keyword,.token.operator,.token.property,.token.symbol{color:#0c969b}.token.tag{color:#994cc3}.token.boolean{color:#bc5454}.token.number{color:#aa0982}.language-css .token.string,.style .token.string,.token.builtin,.token.char,.token.constant,.token.entity,.token.string,.token.url{color:#4876d6}.token.doctype,.token.function,.token.selector{color:#994cc3;font-style:italic}.token.attr-name,.token.inserted{color:#4876d6;font-style:italic}.token.atrule,.token.attr-value,.token.class-name{color:#111}.token.important,.token.regex,.token.variable{color:#c96765}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}`
const style = `<style type="text/css">${css}</style>`
return `${style}${html}`
}
const createRegEx = (input = '') => {
input = input.trim().toLowerCase()
let matcher = input
try {
matcher = new RegExp(input)
} catch (err) {
console.warn("Invalid expression", input)
}
return matcher
}
const fetchAllFileObjects = async () => {
const entries = await fetchTreeObjects();
const limit = 50; // fake limit
const promises = entries.slice(0,limit).map(({name, oid}) => loadScriptBundle(name, oid));
return await Promise.all(promises);
}
const buildPage = (fileObjects) => (input) => {
const matcher = createRegEx(input)
const modals = fileObjects
.filter(({name}) => name.match(matcher) !== null)
.map(buildCodeModal)
const results = `<div style="overflow: hidden;">${modals.join('\n')}</div>`
const metaPanel = `<div class="text-xl font-semibold font-mono pb-2">Found ${modals.length} hits</div>`
const html = `<div>${metaPanel}${results}</div>`
const page = injectCss(html)
console.log(page);
return page
}
const buildScriptRxPanel = async () => {
const objects = await fetchAllFileObjects();
await arg({
message: "Search for scripts:",
input: "",
}, buildPage(objects));
}
await buildScriptRxPanel()
</details>

<details> <summary>Click for code</summary>
// Menu: Kit Emojis
// Description: View + Copy Emojis
// Author: Zach Zeleznick
// Twitter: @zzxiv
// Shortcut: cmd e
const emojisDB = db("emojis", { emojis: {} });
const emojisRef = emojisDB.get("emojis");
// NOTE: Should extract this into a lib since emojis db used in kit-discussions ...
const fetchEmojis = async () => {
// Could install and use as an npm package, but we just need a k-v map ...
const emojiURL = 'https://raw.githubusercontent.com/omnidan/node-emoji/master/lib/emoji.json';
const response = await get(emojiURL);
const emojis = response.data;
emojisDB.set("emojis", emojis).write();
}
const setupEmojis = async () => {
const emojis = emojisRef.value();
if (!emojis || !Object.keys(emojis).length) {
await fetchEmojis()
}
return emojis
}
const createRegEx = (input = '') => {
input = input.trim().toLowerCase()
// NOTE: don't check length here for snappy ux
// input = input.length < 3 ? '' : input
let matcher = input
try {
matcher = new RegExp(input)
} catch (err) {
console.warn("Invalid expression", input)
}
return matcher
}
const showEmojis = (emojis) => (input) => {
const matcher = createRegEx(input)
const inner = Object.entries(emojis)
.filter(([k,v]) => k.match(matcher) !== null)
.map(([k,v]) => `<div class="flex h-10 w-full justify-start items-center">
<div class="text-base font-bold font-sans mr-8"> ${v} </div>
<div class="text-xs font-mono"> ${k} </div>
</div>`).join('\n');
const html = `
<div class="grid grid-cols-1">
${inner}
</div>`
return html
}
const buildEmojisRxPanel = async () => {
const emojis = await setupEmojis();
await arg({
message: "Search for emoji:",
input: "",
}, showEmojis(emojis));
}
const buildEmojisChoices = async () => {
const emojis = await setupEmojis();
const choices = Object.entries(emojis)
.map(([k,v]) => {
return {
name: k,
value: v,
html: `<div> ${v} </div>`
}
});
const emoji = await arg("Search for emoji:", choices);
copy(emoji);
}
const panel = true // would be nice to set based on whether shift is pressed with shortcut cmd
panel ? await buildEmojisRxPanel() : await buildEmojisChoices();
// NOTE: tabs don't play nicely with choices + panels
// onTab("Choices", buildChoicesEmojis);
// onTab("Panel", buildReactivePrompt);
</details> <table> <tr> <td> <img width="476" alt="Emoji Keyboard" src="https://user-images.githubusercontent.com/5779832/113372024-897d7500-931c-11eb-90be-da508ed18d28.png"> </td> <td> <img width="455" alt="Emoji Keyboard Search" src="https://user-images.githubusercontent.com/5779832/113372021-86828480-931c-11eb-8e4f-d714ef39c46e.png"> </td> </tr> <tr> <td> Mac Emoji Keyboard on terminal cursor </td> <td> Mac Emoji Keyboard supports search </td> </tr> </table> <table> <tr> <td> <img width="621" alt="Emoji Character Viewer" src="https://user-images.githubusercontent.com/5779832/113372177-e2e5a400-931c-11eb-821e-97992adb56e9.png"> </td> <td> <img width="616" alt="Emoji Viewer Search" src="https://user-images.githubusercontent.com/5779832/113372171-dfeab380-931c-11eb-8976-7f84725b8b48.png"> </td> </tr> <tr> <td> Mac Emoji Character Viewer Menu </td> <td> Mac Character Viewer Menu also supports search </td> </tr> </table>

<details> <summary> Click for code </summary>
// Menu: Kit Colors
// Description: View CSS Colors
// Author: Zach Zeleznick
// Twitter: @zzxiv
// Inspired by colours.neilorangepeel.com
// Also see colors.commutercreative.com/grid
const colors = {"aliceblue":[240,248,255],"antiquewhite":[250,235,215],"aqua":[0,255,255],"aquamarine":[127,255,212],"azure":[240,255,255],"beige":[245,245,220],"bisque":[255,228,196],"black":[0,0,0],"blanchedalmond":[255,235,205],"blue":[0,0,255],"blueviolet":[138,43,226],"brown":[165,42,42],"burlywood":[222,184,135],"cadetblue":[95,158,160],"chartreuse":[127,255,0],"chocolate":[210,105,30],"coral":[255,127,80],"cornflowerblue":[100,149,237],"cornsilk":[255,248,220],"crimson":[220,20,60],"cyan":[0,255,255],"darkblue":[0,0,139],"darkcyan":[0,139,139],"darkgoldenrod":[184,134,11],"darkgray":[169,169,169],"darkgreen":[0,100,0],"darkgrey":[169,169,169],"darkkhaki":[189,183,107],"darkmagenta":[139,0,139],"darkolivegreen":[85,107,47],"darkorange":[255,140,0],"darkorchid":[153,50,204],"darkred":[139,0,0],"darksalmon":[233,150,122],"darkseagreen":[143,188,143],"darkslateblue":[72,61,139],"darkslategray":[47,79,79],"darkslategrey":[47,79,79],"darkturquoise":[0,206,209],"darkviolet":[148,0,211],"deeppink":[255,20,147],"deepskyblue":[0,191,255],"dimgray":[105,105,105],"dimgrey":[105,105,105],"dodgerblue":[30,144,255],"firebrick":[178,34,34],"floralwhite":[255,250,240],"forestgreen":[34,139,34],"fuchsia":[255,0,255],"gainsboro":[220,220,220],"ghostwhite":[248,248,255],"gold":[255,215,0],"goldenrod":[218,165,32],"gray":[128,128,128],"green":[0,128,0],"greenyellow":[173,255,47],"grey":[128,128,128],"honeydew":[240,255,240],"hotpink":[255,105,180],"indianred":[205,92,92],"indigo":[75,0,130],"ivory":[255,255,240],"khaki":[240,230,140],"lavender":[230,230,250],"lavenderblush":[255,240,245],"lawngreen":[124,252,0],"lemonchiffon":[255,250,205],"lightblue":[173,216,230],"lightcoral":[240,128,128],"lightcyan":[224,255,255],"lightgoldenrodyellow":[250,250,210],"lightgray":[211,211,211],"lightgreen":[144,238,144],"lightgrey":[211,211,211],"lightpink":[255,182,193],"lightsalmon":[255,160,122],"lightseagreen":[32,178,170],"lightskyblue":[135,206,250],"lightslategray":[119,136,153],"lightslategrey":[119,136,153],"lightsteelblue":[176,196,222],"lightyellow":[255,255,224],"lime":[0,255,0],"limegreen":[50,205,50],"linen":[250,240,230],"magenta":[255,0,255],"maroon":[128,0,0],"mediumaquamarine":[102,205,170],"mediumblue":[0,0,205],"mediumorchid":[186,85,211],"mediumpurple":[147,112,219],"mediumseagreen":[60,179,113],"mediumslateblue":[123,104,238],"mediumspringgreen":[0,250,154],"mediumturquoise":[72,209,204],"mediumvioletred":[199,21,133],"midnightblue":[25,25,112],"mintcream":[245,255,250],"mistyrose":[255,228,225],"moccasin":[255,228,181],"navajowhite":[255,222,173],"navy":[0,0,128],"oldlace":[253,245,230],"olive":[128,128,0],"olivedrab":[107,142,35],"orange":[255,165,0],"orangered":[255,69,0],"orchid":[218,112,214],"palegoldenrod":[238,232,170],"palegreen":[152,251,152],"paleturquoise":[175,238,238],"palevioletred":[219,112,147],"papayawhip":[255,239,213],"peachpuff":[255,218,185],"peru":[205,133,63],"pink":[255,192,203],"plum":[221,160,221],"powderblue":[176,224,230],"purple":[128,0,128],"rebeccapurple":[102,51,153],"red":[255,0,0],"rosybrown":[188,143,143],"royalblue":[65,105,225],"saddlebrown":[139,69,19],"salmon":[250,128,114],"sandybrown":[244,164,96],"seagreen":[46,139,87],"seashell":[255,245,238],"sienna":[160,82,45],"silver":[192,192,192],"skyblue":[135,206,235],"slateblue":[106,90,205],"slategray":[112,128,144],"slategrey":[112,128,144],"snow":[255,250,250],"springgreen":[0,255,127],"steelblue":[70,130,180],"tan":[210,180,140],"teal":[0,128,128],"thistle":[216,191,216],"tomato":[255,99,71],"turquoise":[64,224,208],"violet":[238,130,238],"wheat":[245,222,179],"white":[255,255,255],"whitesmoke":[245,245,245],"yellow":[255,255,0],"yellowgreen":[154,205,50]};
const colorGroups = {"pink":["pink","lightpink","hotpink","deeppink","palevioletred","mediumvioletred"],"purple":["lavender","thistle","plum","orchid","violet","fuchsia","magenta","mediumorchid","darkorchid","darkviolet","blueviolet","darkmagenta","purple","mediumpurple","mediumslateblue","slateblue","darkslateblue","rebeccapurple","indigo"],"red":["lightsalmon","salmon","darksalmon","lightcoral","indianred","crimson","red","firebrick","darkred"],"orange":["orange","darkorange","coral","tomato","orangered"],"yellow":["gold","yellow","lightyellow","lemonchiffon","lightgoldenrodyellow","papayawhip","moccasin","peachpuff","palegoldenrod","khaki","darkkhaki"],"green":["greenyellow","chartreuse","lawngreen","lime","limegreen","palegreen","lightgreen","mediumspringgreen","springgreen","mediumseagreen","seagreen","forestgreen","green","darkgreen","yellowgreen","olivedrab","darkolivegreen","mediumaquamarine","darkseagreen","lightseagreen","darkcyan","teal"],"cyan":["aqua","cyan","lightcyan","paleturquoise","aquamarine","turquoise","mediumturquoise","darkturquoise"],"blue":["cadetblue","steelblue","lightsteelblue","lightblue","powderblue","lightskyblue","skyblue","cornflowerblue","deepskyblue","dodgerblue","royalblue","blue","mediumblue","darkblue","navy","midnightblue"],"brown":["cornsilk","blanchedalmond","bisque","navajowhite","wheat","burlywood","tan","rosybrown","sandybrown","goldenrod","darkgoldenrod","peru","chocolate","olive","saddlebrown","sienna","brown","maroon"],"white":["white","snow","honeydew","mintcream","azure","aliceblue","ghostwhite","whitesmoke","seashell","beige","oldlace","floralwhite","ivory","antiquewhite","linen","lavenderblush","mistyrose"],"gray":["gainsboro","lightgray","silver","darkgray","dimgray","gray","lightslategray","slategray","darkslategray","black"]}
const allColors = Object.values(colorGroups).reduce((a,b) => a.concat(b), []);
// Adapted from https://stackoverflow.com/a/3943023
const pickTextColor = ([r, g, b]) => {
const L = r * 0.299 + g * 0.587 + b * 0.114;
return (L > 186) ? "black" : "white";
}
const componentToHex = (v) => v.toString(16).padStart(2, 0)
const rgbToHex = ([r, g, b]) => `#${[r,g,b].map(componentToHex).join('').toUpperCase()}`
const formatRgb = ([r, g, b]) => `rgb(${r},${g},${b})`
const showCategory = (category) => {
const dataset = category ? colorGroups[category] : allColors;
const inner = dataset.map(name => {
const rgb = colors[name]
// console.log(`rgb: ${rgb}, name: ${name}`);
const color = pickTextColor(rgb);
return `<div class="flex flex-col h-20 w-full justify-center items-center" style="background: ${name}">
<div style="color:${color}" class="text-base font-bold font-sans"> ${name.toUpperCase()} </div>
<div style="color:${color}" class="text-xs font-mono"> ${rgbToHex(rgb)} ${formatRgb(rgb)} </div>
</div>`
}).join('\n');
const html = `
<div class="grid grid-cols-1">
${inner}
</div>`
// console.log(`category: ${category}`, html);
return html
}
const buildTabs = () => {
const groups = Object.keys(colorGroups);
let tabs = [ {
name: "All",
method: async () => await arg("all", showCategory())
}];
groups.map(name => {
tabs.push( { name, method: async () => await arg(name, showCategory(name)) } )
});
tabs.map(({name, method}) => {
onTab(name, method);
});
}
// await arg("all", showCategory())
// NOTE: current api requires typing before panel is displayed if we pass in a function like (input) => {}
buildTabs();
</details>

<table> <tr> <td> <img width="525" alt="6 days ago actual" src="https://user-images.githubusercontent.com/5779832/113231006-1a861a80-924f-11eb-9706-d98956b68c40.png"> </td> <td> <img width="556" alt="7 days ago rounded" src="https://user-images.githubusercontent.com/5779832/113231004-18bc5700-924f-11eb-91c9-35448536f6d1.png"> </td> </tr> <tr> <td> March 25th appearing as 6 days ago</d> <td> March 25th appearing as 7 days ago</d> </tr> </table> <details> <summary>Click for (not very clean) code </summary>
// Menu: Kit Discusssions
// Description: View Kit Discussions
// Author: Zach Zeleznick
// Twitter: @zzxiv
const {focusTab} = await kit('chrome')
// const humanizeDuration = await npm('humanize-duration')
const emojisDB = db("emojis", { emojis: {} });
const emojisRef = emojisDB.get("emojis");
const categoriesDB = db("kit-discussions", { categories: [] });
const categoriesRef = categoriesDB.get("categories");
const githubURL = "https://api.github.com/graphql";
let token = env.GITHUB_ACCESS_TOKEN;
if (!token) {
const element = `
<div class="flex flex-col justify-center">
<div>
<a href="https://github.com/settings/tokens/new">Create a token</a> with "public_repo" enabled.
</div>
<br>
<div>
Then, copy + paste the token above or set <code>GITHUB_REPO_TOKEN</code> inside <code>~/.kenv/.env</code>
</div>
</div>`
token = await env("GITHUB_ACCESS_TOKEN", {
info: `Create and enter your personal access token`,
choices: element,
});
}
const fetchEmojis = async () => {
// Could install and use as an npm package, but we just need a k-v map ...
const emojiURL = 'https://raw.githubusercontent.com/omnidan/node-emoji/master/lib/emoji.json';
const response = await get(emojiURL);
const emojis = response.data;
// console.log(JSON.stringify(emojis, null, 2));
emojisDB.set("emojis", emojis).write();
}
const setupEmojis = async () => {
const emojis = emojisRef.value();
if (!emojis || !Object.keys(emojis).length) {
await fetchEmojis()
}
return emojis
}
const lookupEmoji = (key) => {
const emojis = emojisRef.value();
return emojis[key.slice(1, key.length - 1)]
}
const config = {
headers: {
"Authorization": `Bearer ${token}`,
"GraphQL-Features": "discussions_api",
}
}
const categoriesQuery = `
query {
repository(owner: "johnlindquist", name: "kit") {
discussionCategories(first: 10) {
# type: DiscussionConnection
totalCount # Int!
nodes {
id,
name,
emoji,
# emojiHTML,
description,
}
}
}
}`
const fetchCategories = async () => {
let response;
try {
response = await post(githubURL,
{
query: categoriesQuery
},
config
);
}
catch (err) {
console.warn("fetchCategories failed:", err);
return
}
const graphqlResponse = response.data;
// console.log(JSON.stringify(categories, null, 2));
const {
data: {
repository: {
discussionCategories: {
totalCount,
nodes
}
}
}
} = graphqlResponse;
categoriesDB.set("categories", nodes).write();
}
const setupCategories = async () => {
const categories = categoriesRef.value();
if (!categories || !categories.length) {
await fetchCategories()
}
return categories
}
// NOTE: can use `categoryId` in discussions query
// to limit results or could just fetch all and filter
const discussionInnerQuery = `
# type: DiscussionConnection
totalCount # Int!
nodes {
# type: Discussion
id,
title,
# bodyText,
createdAt,
resourcePath,
category {
id,
name,
emoji,
},
author {
login,
# avatarUrl,
}
}
`
const allDiscussionsQuery = `
query {
repository(owner: "johnlindquist", name: "kit") {
discussions(first: 10, orderBy: {
field: CREATED_AT,
direction: DESC,
}) {
${discussionInnerQuery}
}
}
}`
const buildCategoryQuery = (categoryId) => `
query {
repository(owner: "johnlindquist", name: "kit") {
discussions(first: 10, categoryId: "${categoryId}", orderBy: {
field: CREATED_AT,
direction: DESC,
}) {
${discussionInnerQuery}
}
}
}`
const fetchDiscussions = async (categoryId = "") => {
let response;
const query = categoryId ? buildCategoryQuery(categoryId) : allDiscussionsQuery;
try {
response = await post(githubURL,
{
query,
},
config
);
}
catch (err) {
console.warn("fetchDiscussions failed:", err);
return
}
const {data, errors } = response.data;
if (errors) {
console.warn("fetchDiscussions errors:", errors);
// todo: handle errors
}
const {
repository: {
discussions: {
totalCount,
nodes
}
}
} = data;
// console.log(JSON.stringify(nodes, null, 2));
return nodes;
}
const allDiscussions = async () => await fetchDiscussions();
const buildHtml = ({emoji}) => {
const glyph = lookupEmoji(emoji)
return `<div class="flex justify-center">
<div> ${glyph} </div>
</div>
`
}
const humanizeDuration = (duration) => {
// intend to mirror `humanizeDuration(duration, { round: true, largest: 1 })`
// note that 36 hours (1.5 days) would round to 2 days which isn't always the goal
// e.g.
// '2021-03-30T06:00:00Z' <> '2021-03-31T18:00:00Z' 36 hours, expect 1 vs 2
// '2021-03-30T18:00:00Z' <> '2021-04-01T06:00:00Z' 36 hours, expect 2
const components = {
"seconds": 1000,
"minutes": 60000,
"hours": 3600000,
"days": 86400000,
}
const units = Object.keys(components);
for (let i = units.length - 1; i > -1; i--) {
let unit = units[i];
const divisor = components[unit];
const val = duration / divisor;
const fval = Math.floor(val);
const rval = Math.round(val);
if (fval === 0) {
continue
}
unit = rval === 1 ? unit.slice(0, unit.length -1) : unit;
return `${rval} ${unit}`
}
}
const humanizeTime = (createdAt, fakeTime) => {
const then = new Date(createdAt);
const now = fakeTime ? new Date(fakeTime) : new Date();
let duration = now - then; // implicitly calls getTime();
// NOTE: Github UI rounds (so this interesting)
if (duration > 86400000) { // handle rounding case for days
const loffset = (then.getHours() - 12) * 3600000;
const roffset = (12 - now.getHours()) * 3600000;
duration = duration + loffset + roffset;
}
if (duration < 2592000000) { // within 30 days (in ms)
return `${humanizeDuration(duration)} ago`;
}
const sameYear = now.getYear() === then.getYear();
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const options = { timeZone, year: sameYear ? undefined : 'numeric', month: 'short', day: 'numeric' };
return `on ${then.toLocaleDateString('en-US', options)}`
}
const buildChoice = (node) => {
const {
title,
resourcePath,
createdAt,
category: {
name,
emoji,
},
author: {
login
},
} = node;
const url = `https://github.com${resourcePath}`
const description = `${login} created ${humanizeTime(createdAt)} in ${name}`
const html = buildHtml({emoji})
return {
name: title,
value: url,
description,
html,
}
}
const showCategory = async (categoryId) => {
const nodes = await fetchDiscussions(categoryId);
const choices = nodes.map(buildChoice);
const selectedIssue = await arg("Search discussions:", choices);
focusTab(selectedIssue);
}
const buildTabs = async () => {
const categories = await setupCategories();
let tabs = [ {
name: "All",
method: showCategory
}];
categories.map(({name, id}) => {
tabs.push( { name, method: async () => await showCategory(id) } )
});
tabs.map(({name, method}) => {
onTab(name, method);
});
}
await setupEmojis();
buildTabs();
</details>

<details> <summary> Click for code </summary>
// Menu: Pokedex
// Description: Display Pokemon
// Author: Zach Zeleznick
// Twitter: @zzxiv
const {focusTab} = await kit('chrome')
const pokemon = [{"name":"bulbasaur","id":"1"},{"name":"charmander","id":"4"},{"name":"squirtle","id":"7"},{"name":"caterpie","id":"10"},{"name":"weedle","id":"13"},{"name":"pidgey","id":"16"},{"name":"rattata","id":"19"},{"name":"spearow","id":"21"},{"name":"ekans","id":"23"},{"name":"sandshrew","id":"27"},{"name":"nidoran♀","id":"29"},{"name":"nidoran♂","id":"32"},{"name":"vulpix","id":"37"},{"name":"zubat","id":"41"},{"name":"oddish","id":"43"},{"name":"paras","id":"46"},{"name":"venonat","id":"48"},{"name":"diglett","id":"50"},{"name":"meowth","id":"52"},{"name":"psyduck","id":"54"},{"name":"mankey","id":"56"},{"name":"growlithe","id":"58"},{"name":"poliwag","id":"60"},{"name":"abra","id":"63"},{"name":"machop","id":"66"},{"name":"bellsprout","id":"69"},{"name":"tentacool","id":"72"},{"name":"geodude","id":"74"},{"name":"venusaur","id":"3"},{"name":"charmeleon","id":"5"},{"name":"charizard","id":"6"},{"name":"wartortle","id":"8"},{"name":"blastoise","id":"9"},{"name":"metapod","id":"11"},{"name":"butterfree","id":"12"},{"name":"kakuna","id":"14"},{"name":"beedrill","id":"15"},{"name":"pidgeotto","id":"17"},{"name":"pidgeot","id":"18"},{"name":"raticate","id":"20"},{"name":"fearow","id":"22"},{"name":"arbok","id":"24"},{"name":"pikachu","id":"25"},{"name":"raichu","id":"26"},{"name":"sandslash","id":"28"},{"name":"nidorina","id":"30"},{"name":"nidoqueen","id":"31"},{"name":"nidorino","id":"33"},{"name":"nidoking","id":"34"},{"name":"clefairy","id":"35"},{"name":"clefable","id":"36"},{"name":"ninetales","id":"38"},{"name":"jigglypuff","id":"39"},{"name":"wigglytuff","id":"40"},{"name":"golbat","id":"42"},{"name":"gloom","id":"44"},{"name":"vileplume","id":"45"},{"name":"parasect","id":"47"},{"name":"venomoth","id":"49"},{"name":"dugtrio","id":"51"},{"name":"persian","id":"53"},{"name":"golduck","id":"55"},{"name":"primeape","id":"57"},{"name":"arcanine","id":"59"},{"name":"poliwhirl","id":"61"},{"name":"poliwrath","id":"62"},{"name":"kadabra","id":"64"},{"name":"alakazam","id":"65"},{"name":"machoke","id":"67"},{"name":"machamp","id":"68"},{"name":"weepinbell","id":"70"},{"name":"victreebel","id":"71"},{"name":"tentacruel","id":"73"},{"name":"graveler","id":"75"},{"name":"ponyta","id":"77"},{"name":"slowpoke","id":"79"},{"name":"magnemite","id":"81"},{"name":"farfetchd","id":"83"},{"name":"doduo","id":"84"},{"name":"seel","id":"86"},{"name":"grimer","id":"88"},{"name":"shellder","id":"90"},{"name":"gastly","id":"92"},{"name":"onix","id":"95"},{"name":"drowzee","id":"96"},{"name":"krabby","id":"98"},{"name":"voltorb","id":"100"},{"name":"exeggcute","id":"102"},{"name":"cubone","id":"104"},{"name":"lickitung","id":"108"},{"name":"koffing","id":"109"},{"name":"rhyhorn","id":"111"},{"name":"tangela","id":"114"},{"name":"kangaskhan","id":"115"},{"name":"horsea","id":"116"},{"name":"goldeen","id":"118"},{"name":"staryu","id":"120"},{"name":"scyther","id":"123"},{"name":"pinsir","id":"127"},{"name":"tauros","id":"128"},{"name":"magikarp","id":"129"},{"name":"lapras","id":"131"},{"name":"ditto","id":"132"},{"name":"eevee","id":"133"},{"name":"porygon","id":"137"},{"name":"omanyte","id":"138"},{"name":"kabuto","id":"140"},{"name":"aerodactyl","id":"142"},{"name":"articuno","id":"144"},{"name":"zapdos","id":"145"},{"name":"moltres","id":"146"},{"name":"dratini","id":"147"},{"name":"mewtwo","id":"150"},{"name":"rapidash","id":"78"},{"name":"slowbro","id":"80"},{"name":"magneton","id":"82"},{"name":"dodrio","id":"85"},{"name":"dewgong","id":"87"},{"name":"muk","id":"89"},{"name":"cloyster","id":"91"},{"name":"haunter","id":"93"},{"name":"gengar","id":"94"},{"name":"hypno","id":"97"},{"name":"kingler","id":"99"},{"name":"electrode","id":"101"},{"name":"exeggutor","id":"103"},{"name":"marowak","id":"105"},{"name":"hitmonlee","id":"106"},{"name":"hitmonchan","id":"107"},{"name":"weezing","id":"110"},{"name":"rhydon","id":"112"},{"name":"chansey","id":"113"},{"name":"seadra","id":"117"},{"name":"seaking","id":"119"},{"name":"starmie","id":"121"},{"name":"mr-mime","id":"122"},{"name":"jynx","id":"124"},{"name":"electabuzz","id":"125"},{"name":"magmar","id":"126"},{"name":"gyarados","id":"130"},{"name":"vaporeon","id":"134"},{"name":"jolteon","id":"135"},{"name":"flareon","id":"136"},{"name":"omastar","id":"139"},{"name":"kabutops","id":"141"},{"name":"snorlax","id":"143"},{"name":"dragonair","id":"148"},{"name":"dragonite","id":"149"},{"name":"mew","id":"151"},{"name":"ivysaur","id":"2"},{"name":"golem","id":"76"}]
const translations = {
"mr-mime": "Mr._Mime",
"farfetchd": "Farfetch'd"
}
const toTitleCase = (str) => {
return str.split(' ').map(s => s.charAt(0).toUpperCase() + s.substr(1).toLowerCase()).join(' ');
}
const buildImageUrl = (id) => {
// e.g. https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png
const baseUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon"
return `${baseUrl}/${id}.png`
}
const buildOpenUrl = (name) => {
// e.g. https://bulbapedia.bulbagarden.net/wiki/Nidoran%E2%99%80_(Pok%C3%A9mon)
const baseUrl = "https://bulbapedia.bulbagarden.net/wiki"
let translated = translations[name] ? translations[name] : toTitleCase(name);
return encodeURI(`${baseUrl}/${translated}_(Pokémon)`);
}
const buildChoices = () => {
return pokemon.map(({name, id}) => {
return {
name: toTitleCase(name),
value: name,
description: `Pokedex entry: ${id.padStart(3, 0)}`,
img: buildImageUrl(id)
}
});
}
const name = await arg("View Gen 1 Pokemon:", buildChoices());
focusTab(buildOpenUrl(name));
</details>