import { action, defineExtension, detail, type DetailView, type Item } from '@qikbar/qikbar'
interface Place {
name: string
country?: string
admin1?: string
latitude: number
longitude: number
timezone: string
}
const CODES: Record<number, [string, string]> = {
0: ['Clear sky', '☀️'], 1: ['Mainly clear', '🌤️'], 2: ['Partly cloudy', '⛅'], 3: ['Overcast', '☁️'],
45: ['Fog', '🌫️'], 48: ['Rime fog', '🌫️'], 51: ['Light drizzle', '🌦️'], 53: ['Drizzle', '🌦️'], 55: ['Heavy drizzle', '🌧️'],
61: ['Light rain', '🌦️'], 63: ['Rain', '🌧️'], 65: ['Heavy rain', '🌧️'], 71: ['Light snow', '🌨️'], 73: ['Snow', '🌨️'],
75: ['Heavy snow', '❄️'], 80: ['Showers', '🌦️'], 81: ['Heavy showers', '🌧️'], 82: ['Violent showers', '⛈️'], 95: ['Thunderstorm', '⛈️'],
96: ['Thunderstorm with hail', '⛈️'], 99: ['Thunderstorm with hail', '⛈️'],
}
const describe = (code: number) => CODES[code] ?? ['Unknown', '🌡️']
async function geocode(name: string, signal?: AbortSignal): Promise<Place[]> {
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?count=5&name=${encodeURIComponent(name)}`, { signal })
const data = await res.json()
return data.results ?? []
}
async function forecast(place: Place, unit: 'celsius' | 'fahrenheit', signal?: AbortSignal) {
const params = new URLSearchParams({
latitude: String(place.latitude),
longitude: String(place.longitude),
timezone: place.timezone || 'auto',
temperature_unit: unit,
current: 'temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code',
daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max',
forecast_days: '7',
})
return fetch(`https://api.open-meteo.com/v1/forecast?${params}`, { signal }).then((r) => r.json())
}
const label = (p: Place) => [p.name, p.admin1, p.country].filter(Boolean).join(', ')
function view(place: Place, unit: 'celsius' | 'fahrenheit'): DetailView {
const deg = unit === 'celsius' ? '°C' : '°F'
let summary = ''
return detail({
title: place.name,
markdown: async () => {
const f = await forecast(place, unit)
const c = f.current
const [text, emoji] = describe(c.weather_code)
const days = f.daily.time.map((day: string, i: number) => {
const [t, e] = describe(f.daily.weather_code[i])
const name = new Date(`${day}T12:00`).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
return `| ${name} | ${e} ${t} | ${Math.round(f.daily.temperature_2m_max[i])}${deg} | ${Math.round(f.daily.temperature_2m_min[i])}${deg} | ${f.daily.precipitation_probability_max[i] ?? 0}% |`
})
summary = `${label(place)}: ${Math.round(c.temperature_2m)}${deg}, ${text.toLowerCase()}`
return [
`# ${emoji} ${Math.round(c.temperature_2m)}${deg}`,
`**${text}** in ${label(place)}. Feels like ${Math.round(c.apparent_temperature)}${deg}, humidity ${c.relative_humidity_2m}%, wind ${Math.round(c.wind_speed_10m)} km/h.`,
'',
'| Day | Conditions | High | Low | Rain |',
'| --- | --- | --- | --- | --- |',
...days,
'',
'_Data: [Open-Meteo](https://open-meteo.com)_',
].join('\n')
},
metadata: [
{ label: 'Location', value: label(place) },
{ label: 'Coordinates', value: `${place.latitude.toFixed(2)}, ${place.longitude.toFixed(2)}` },
{ label: 'Timezone', value: place.timezone },
],
actions: [
action.copy(() => summary, { title: 'Copy Summary' }),
action.open(`https://www.google.com/maps/@${place.latitude},${place.longitude},11z`, { title: 'Open in Maps', shortcut: 'mod+o' }),
],
})
}
export default defineExtension({
name: 'weather',
title: 'Weather',
icon: '⛅',
description: 'Current conditions and a 7-day forecast for any city. Type “weather in tokyo”.',
preferences: [
{
name: 'unit',
title: 'Temperature unit',
type: 'select',
default: 'celsius',
options: [
{ title: 'Celsius', value: 'celsius' },
{ title: 'Fahrenheit', value: 'fahrenheit' },
],
},
],
commands: [
{
name: 'forecast',
title: 'Weather Forecast',
icon: '⛅',
keywords: ['weather', 'temperature', 'rain', 'forecast'],
arguments: [{ name: 'city', placeholder: 'City', required: true }],
async run(ctx) {
const [place] = await geocode(ctx.arguments.city!, ctx.signal)
if (!place) {
ctx.toast({ title: `Couldn’t find “${ctx.arguments.city}”`, style: 'failure' })
return false
}
return view(place, ctx.preferences.unit)
},
},
],
sources: [
{
id: 'weather-in',
title: 'Weather',
position: 'top',
debounce: 250,
async search({ query, signal, ctx }) {
const m = query.match(/^(?:weather|forecast|temperature)\s+(?:in|for|at)\s+(.{2,})$/i)
if (!m) return []
const places = await geocode(m[1]!, signal)
return places.slice(0, 3).map<Item>((p) => ({
id: `weather:${p.latitude},${p.longitude}`,
title: `Weather in ${p.name}`,
subtitle: [p.admin1, p.country].filter(Boolean).join(', '),
icon: '⛅',
noHistory: true,
view: () => view(p, ctx.preferences.unit ?? 'celsius'),
}))
},
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
const API = 'https://api.frankfurter.dev/v1'
let names: Promise<Record<string, string>> | undefined
const currencies = () => (names ??= fetch(`${API}/currencies`).then((r) => r.json()))
const fmt = (value: number, code: string) => {
try {
return new Intl.NumberFormat(undefined, { style: 'currency', currency: code, maximumFractionDigits: value < 1 ? 4 : 2 }).format(value)
} catch {
return `${value.toFixed(2)} ${code}`
}
}
export default defineExtension({
name: 'currency',
title: 'Currency',
icon: '💱',
description: 'Convert between 30+ currencies with daily ECB rates. Type “100 usd to eur”.',
sources: [
{
id: 'currency-convert',
title: 'Currency',
position: 'top',
debounce: 200,
async search({ query, signal }) {
const m = query.trim().match(/^([\d.,]+)\s*([a-z]{3})\s+(?:to|in|=)\s+([a-z]{3})$/i)
if (!m) return []
const amount = Number(m[1]!.replace(/,/g, ''))
const from = m[2]!.toUpperCase()
const to = m[3]!.toUpperCase()
if (!Number.isFinite(amount) || from === to) return []
const res = await fetch(`${API}/latest?base=${from}&symbols=${to}`, { signal })
if (!res.ok) return []
const data = await res.json()
const rate: number | undefined = data.rates?.[to]
if (!rate) return []
const result = amount * rate
const all = await currencies()
return [
{
id: 'currency:result',
title: fmt(result, to),
subtitle: `${fmt(amount, from)} · 1 ${from} = ${rate} ${to}`,
icon: '💱',
noHistory: true,
accessories: [{ text: `${all[to] ?? to}` }, { tag: data.date }],
actions: [
action.copy(result.toFixed(2), { title: 'Copy Amount', shortcut: undefined }),
action.copy(`${fmt(amount, from)} = ${fmt(result, to)}`, { title: 'Copy Conversion', shortcut: 'mod+shift+c' }),
],
},
]
},
},
],
commands: [
{
name: 'rates',
title: 'Exchange Rates',
icon: '💱',
keywords: ['currency', 'forex', 'fx', 'money'],
arguments: [{ name: 'base', placeholder: 'Base (USD)' }],
view: (ctx) => {
const base = (ctx.arguments.base || 'USD').toUpperCase()
return list({
title: `Rates for ${base}`,
placeholder: 'Filter currencies…',
filter: true,
items: async ({ signal }) => {
const [data, all] = await Promise.all([fetch(`${API}/latest?base=${base}`, { signal }).then((r) => r.json()), currencies()])
if (!data.rates) throw new Error(`Unknown currency “${base}”`)
return Object.entries(data.rates as Record<string, number>).map<Item>(([code, rate]) => ({
id: code,
title: code,
subtitle: all[code],
keywords: [all[code] ?? ''],
icon: '💱',
accessories: [{ text: `1 ${base} = ${rate}` }],
actions: [action.copy(String(rate), { title: 'Copy Rate' })],
}))
},
})
},
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
interface Result {
package: {
name: string
version: string
description?: string
date: string
links: { npm: string; homepage?: string; repository?: string }
publisher?: { username: string }
keywords?: string[]
}
downloads?: { weekly: number }
}
const compact = (n: number) => new Intl.NumberFormat(undefined, { notation: 'compact' }).format(n)
export default defineExtension({
name: 'npm',
title: 'npm',
icon: 'package',
description: 'Search the npm registry and copy install commands for npm, pnpm, yarn or bun.',
preferences: [
{
name: 'pm',
title: 'Package manager',
type: 'select',
default: 'npm',
options: ['npm', 'pnpm', 'yarn', 'bun'].map((v) => ({ title: v, value: v })),
},
],
commands: [
{
name: 'search',
title: 'Search npm Packages',
icon: 'package',
keywords: ['npm', 'node', 'package', 'library', 'install'],
view: (ctx) =>
list({
title: 'npm',
placeholder: 'Search packages…',
detail: true,
debounce: 200,
emptyView: { title: 'Search npm', description: 'Find a package by name or keyword', icon: 'package' },
items: async ({ query, signal }) => {
if (!query.trim()) return []
const data = await fetch(`https://registry.npmjs.org/-/v1/search?size=20&text=${encodeURIComponent(query)}`, { signal }).then((r) => r.json())
const pm: string = ctx.preferences.pm ?? 'npm'
const install = (name: string, tool = pm) => (tool === 'npm' ? `npm install ${name}` : tool === 'yarn' ? `yarn add ${name}` : `${tool} add ${name}`)
return (data.objects as Result[]).map<Item>(({ package: p, downloads }) => ({
id: p.name,
title: p.name,
subtitle: p.description,
icon: 'package',
url: p.links.npm,
keywords: p.keywords,
accessories: [
...(downloads?.weekly ? [{ text: `${compact(downloads.weekly)}/wk`, icon: 'download' }] : []),
{ tag: `v${p.version}` },
],
detail: [
`# ${p.name}`,
p.description ?? '',
'',
'```bash',
install(p.name),
'```',
'',
`| | |`,
`| --- | --- |`,
`| Version | ${p.version} |`,
`| Published | ${new Date(p.date).toLocaleDateString()} |`,
downloads?.weekly ? `| Weekly downloads | ${downloads.weekly.toLocaleString()} |` : '',
p.publisher ? `| Publisher | ${p.publisher.username} |` : '',
p.links.repository ? `| Repository | [${p.links.repository.replace(/^https?:\/\//, '')}](${p.links.repository}) |` : '',
p.links.homepage ? `| Homepage | [${p.links.homepage.replace(/^https?:\/\//, '')}](${p.links.homepage}) |` : '',
]
.filter((l) => l !== '')
.join('\n'),
actions: [
action.copy(install(p.name), { title: `Copy ${pm} Install`, shortcut: 'mod+c' }),
...['npm', 'pnpm', 'yarn', 'bun']
.filter((t) => t !== pm)
.map((t) => action.copy(install(p.name, t), { title: `Copy ${t} Install`, shortcut: undefined, section: 'Install' })),
...(p.links.repository ? [action.open(p.links.repository, { title: 'Open Repository', shortcut: 'mod+o' })] : []),
action.open(`https://bundlephobia.com/package/${p.name}`, { title: 'Check Bundle Size', icon: 'bar-chart', shortcut: undefined }),
],
}))
},
}),
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
interface Hit {
title: string
snippet: string
pageid: number
wordcount: number
timestamp: string
}
const strip = (html: string) => html.replace(/<[^>]+>/g, '').replace(/"/g, '"').replace(/&/g, '&').replace(/'/g, "'")
async function summary(title: string): Promise<string> {
const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title.replace(/ /g, '_'))}`)
if (!res.ok) return `# ${title}\n\nNo summary available.`
const s = await res.json()
const image = s.thumbnail?.source ? `\n\n` : ''
return `${image}# ${s.title}\n\n${s.description ? `*${s.description}*\n\n` : ''}${s.extract ?? ''}`
}
export default defineExtension({
name: 'wikipedia',
title: 'Wikipedia',
icon: 'https://en.wikipedia.org/static/favicon/wikipedia.ico',
description: 'Search Wikipedia with a live article summary next to the results.',
preferences: [
{ name: 'lang', title: 'Language', type: 'text', default: 'en', placeholder: 'en, de, fr, ja…', description: 'Wikipedia language code for links.' },
],
commands: [
{
name: 'search',
title: 'Search Wikipedia',
icon: 'https://en.wikipedia.org/static/favicon/wikipedia.ico',
keywords: ['wiki', 'encyclopedia', 'lookup'],
view: (ctx) =>
list({
title: 'Wikipedia',
placeholder: 'Search Wikipedia…',
detail: true,
debounce: 250,
emptyView: { title: 'Search Wikipedia', description: 'Start typing to search 6 million articles', icon: 'book' },
items: async ({ query, signal }) => {
if (query.trim().length < 2) return []
const url = `https://en.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srlimit=20&srsearch=${encodeURIComponent(query)}`
const data = await fetch(url, { signal }).then((r) => r.json())
const lang = ctx.preferences.lang || 'en'
return (data.query?.search as Hit[]).map<Item>((h) => {
const href = `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(h.title.replace(/ /g, '_'))}`
return {
id: String(h.pageid),
title: h.title,
subtitle: strip(h.snippet),
icon: 'book',
url: href,
detail: () => summary(h.title),
accessories: [{ text: `${h.wordcount.toLocaleString()} words` }],
actions: [
action.ask(`Explain the Wikipedia article “${h.title}” in simple terms, with the key facts.`, { title: 'Ask AI About This' }),
],
}
})
},
}),
},
],
})
import { defineExtension, form, list, type Context, type Item, type ListView, type ScopedStorage } from '@qikbar/qikbar'
interface Bookmark {
id: string
title: string
url: string
tags: string[]
created: number
}
const load = (storage: ScopedStorage) => storage.get<Bookmark[]>('bookmarks') ?? []
const save = (storage: ScopedStorage, bookmarks: Bookmark[]) => storage.set('bookmarks', bookmarks)
const favicon = (url: string) => {
try {
return `https://icons.duckduckgo.com/ip3/${new URL(url).hostname}.ico`
} catch {
return 'bookmark'
}
}
function toItem(b: Bookmark, ctx: Context, refresh?: () => void): Item {
return {
id: `bookmark:${b.id}`,
title: b.title,
subtitle: b.url.replace(/^https?:\/\/(www\.)?/, ''),
icon: favicon(b.url),
url: b.url,
keywords: b.tags,
section: 'Bookmarks',
accessories: [...b.tags.map((t) => ({ tag: t })), { date: b.created }],
actions: [
{
title: 'Delete Bookmark',
icon: 'trash',
style: 'destructive',
shortcut: 'mod+backspace',
async run(c) {
if (!(await c.confirm({ title: `Delete “${b.title}”?`, destructive: true, confirmLabel: 'Delete' }))) return false
save(ctx.storage, load(ctx.storage).filter((x) => x.id !== b.id))
c.toast({ title: 'Bookmark deleted', style: 'success' })
refresh?.()
return false
},
},
],
}
}
function addForm(ctx: Context, initial: Partial<Bookmark> = {}) {
return form({
title: 'Add Bookmark',
submitLabel: 'Save Bookmark',
fields: [
{ name: 'url', label: 'URL', type: 'url', required: true, placeholder: 'https://…', defaultValue: initial.url ?? '' },
{ name: 'title', label: 'Title', placeholder: 'Defaults to the domain', defaultValue: initial.title ?? '' },
{ name: 'tags', label: 'Tags', placeholder: 'design, reading', description: 'Comma separated. Tags are searchable.' },
],
onSubmit(values, c) {
let url = String(values.url).trim()
if (!/^[a-z]+:\/\//i.test(url)) url = `https://${url}`
const bookmark: Bookmark = {
id: crypto.randomUUID(),
url,
title: String(values.title).trim() || new URL(url).hostname.replace(/^www\./, ''),
tags: String(values.tags ?? '')
.split(',')
.map((t) => t.trim())
.filter(Boolean),
created: Date.now(),
}
save(ctx.storage, [bookmark, ...load(ctx.storage)])
c.toast({ title: 'Bookmark saved', message: bookmark.title, style: 'success' })
},
})
}
function browse(ctx: Context): ListView {
const refresh = () => ctx.render(browse(ctx))
return list({
title: 'Bookmarks',
placeholder: 'Search bookmarks…',
items: load(ctx.storage).map((b) => ({ ...toItem(b, ctx, refresh), section: undefined })),
emptyView: { title: 'No bookmarks yet', description: 'Press ↵ to add your first one', icon: 'bookmark' },
actions: [{ title: 'Add Bookmark', icon: 'plus', shortcut: 'alt+n', run: () => addForm(ctx) }],
})
}
export default defineExtension({
name: 'bookmarks',
title: 'Bookmarks',
icon: 'bookmark',
description: 'Save links with tags. They live in your browser and show up in root search.',
commands: [
{ name: 'browse', title: 'Browse Bookmarks', icon: 'bookmark', keywords: ['links', 'saved', 'favorites'], view: (ctx) => browse(ctx) },
{
name: 'add',
title: 'Add Bookmark',
icon: 'plus',
keywords: ['save', 'link', 'url'],
view: (ctx) => addForm(ctx, { url: location.href, title: document.title }),
},
],
sources: [
{
id: 'bookmarks',
title: 'Bookmarks',
debounce: 0,
filter: true,
limit: 5,
search: ({ ctx }) => load(ctx.storage).map((b) => toItem(b, ctx)),
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
interface Repo {
full_name: string
description: string | null
html_url: string
stargazers_count: number
language: string | null
owner: { avatar_url: string }
pushed_at: string
}
export default defineExtension({
name: 'github',
title: 'GitHub',
icon: 'github',
description: 'Search public repositories. Add a token for higher rate limits.',
preferences: [
{ name: 'token', title: 'Access token', type: 'password', description: 'Optional. Raises the rate limit from 10 to 30 searches a minute.' },
],
commands: [
{
name: 'search-repos',
title: 'Search Repositories',
icon: 'github',
keywords: ['git', 'code', 'repo'],
view: (ctx) =>
list({
title: 'Repositories',
placeholder: 'Search GitHub repositories…',
debounce: 300,
emptyView: { title: 'Search GitHub', description: 'Type a repository name or topic', icon: 'github' },
items: async ({ query, signal }): Promise<Item[]> => {
if (query.trim().length < 2) return []
const res = await fetch(`https://api.github.com/search/repositories?per_page=20&q=${encodeURIComponent(query)}`, {
signal,
headers: ctx.preferences.token ? { authorization: `Bearer ${ctx.preferences.token}` } : {},
})
if (!res.ok) throw new Error(res.status === 403 ? 'Rate limited. Add a token in Configure GitHub.' : `GitHub error ${res.status}`)
const { items } = (await res.json()) as { items: Repo[] }
return items.map((r) => ({
id: r.full_name,
title: r.full_name,
subtitle: r.description ?? undefined,
icon: { url: r.owner.avatar_url },
url: r.html_url,
accessories: [...(r.language ? [{ tag: r.language }] : []), { text: `★ ${r.stargazers_count.toLocaleString()}` }],
actions: [action.copy(`git clone ${r.html_url}.git`, { title: 'Copy Clone Command', shortcut: 'mod+shift+.' })],
}))
},
}),
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
interface Story {
id: number
title: string
url?: string
score: number
by: string
time: number
descendants?: number
}
const API = 'https://hacker-news.firebaseio.com/v0'
async function stories(kind: 'top' | 'new' | 'best', signal: AbortSignal): Promise<Item[]> {
const ids: number[] = await fetch(`${API}/${kind}stories.json`, { signal }).then((r) => r.json())
const items: Story[] = await Promise.all(ids.slice(0, 30).map((id) => fetch(`${API}/item/${id}.json`, { signal }).then((r) => r.json())))
return items.filter(Boolean).map((s) => {
const comments = `https://news.ycombinator.com/item?id=${s.id}`
return {
id: String(s.id),
title: s.title,
subtitle: s.url ? new URL(s.url).hostname.replace(/^www\./, '') : 'news.ycombinator.com',
icon: `https://icons.duckduckgo.com/ip3/${s.url ? new URL(s.url).hostname : 'news.ycombinator.com'}.ico`,
url: s.url ?? comments,
accessories: [{ text: `▲ ${s.score}` }, { text: `${s.descendants ?? 0}`, icon: 'message' }, { date: s.time * 1000 }],
actions: [action.open(comments, { title: 'Open Comments', shortcut: 'mod+o' }), action.copy(s.url ?? comments, { title: 'Copy Link' })],
}
})
}
export default defineExtension({
name: 'hacker-news',
title: 'Hacker News',
icon: 'https://news.ycombinator.com/favicon.ico',
description: 'Browse top, new and best stories.',
author: 'qikbar',
commands: (['top', 'new', 'best'] as const).map((kind) => ({
name: kind,
title: `${kind[0]!.toUpperCase()}${kind.slice(1)} Stories`,
subtitle: 'Hacker News',
keywords: ['hn', 'news', 'ycombinator'],
view: list({ title: `${kind[0]!.toUpperCase()}${kind.slice(1)} Stories`, filter: true, items: ({ signal }) => stories(kind, signal) }),
})),
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
function parse(input: string): [number, number, number] | undefined {
const s = input.trim().toLowerCase()
let m = s.match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/)
if (m) {
const hex = m[1]!.length === 3 ? [...m[1]!].map((c) => c + c).join('') : m[1]!
return [0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number]
}
m = s.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/)
if (m) return [Number(m[1]), Number(m[2]), Number(m[3])]
m = s.match(/^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/)
if (m) return hslToRgb(Number(m[1]), Number(m[2]) / 100, Number(m[3]) / 100)
return undefined
}
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
const k = (n: number) => (n + h / 30) % 12
const a = s * Math.min(l, 1 - l)
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))
return [f(0), f(8), f(4)].map((v) => Math.round(v * 255)) as [number, number, number]
}
function rgbToHsl([r, g, b]: [number, number, number]) {
;[r, g, b] = [r / 255, g / 255, b / 255]
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const l = (max + min) / 2
if (max === min) return [0, 0, Math.round(l * 100)]
const d = max - min
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
const h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4
return [Math.round(h * 60), Math.round(s * 100), Math.round(l * 100)]
}
/** Every format for a color, as list items (empty if the input isn't a color). */
function formats(input: string): Item[] {
const rgb = parse(input)
if (!rgb) return []
const hex = `#${rgb.map((v) => v.toString(16).padStart(2, '0')).join('')}`
const [h, s, l] = rgbToHsl(rgb)
const values: [string, string][] = [
['HEX', hex],
['RGB', `rgb(${rgb.join(' ')})`],
['HSL', `hsl(${h} ${s}% ${l}%)`],
['CSS var', `--color: ${hex};`],
]
return values.map<Item>(([label, value]) => ({
id: `color:${label}`,
title: value,
subtitle: label,
noHistory: true,
// A filled swatch. SVG strings are rendered as trusted markup, so only use values you control.
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><rect x="1" y="1" width="16" height="16" rx="5" fill="${hex}" stroke="rgba(127,127,127,.35)"/></svg>`,
accessories: [{ kbd: 'enter', text: 'Copy' }],
actions: [action.copy(value, { title: `Copy ${label}`, shortcut: undefined })],
}))
}
const randomHex = () => `#${[...crypto.getRandomValues(new Uint8Array(3))].map((v) => v.toString(16).padStart(2, '0')).join('')}`
export default defineExtension({
name: 'colors',
title: 'Colors',
icon: 'palette',
description: 'Convert colors between hex, RGB and HSL. Type a color like “#22d3ee”, or run Convert Color.',
commands: [
{
name: 'convert',
title: 'Convert Color',
subtitle: 'Hex, RGB, HSL',
icon: 'palette',
keywords: ['color', 'colour', 'hex', 'rgb', 'hsl', 'css'],
arguments: [{ name: 'color', placeholder: '#22d3ee or rgb(…)', required: true }],
run(ctx) {
const items = formats(ctx.arguments.color ?? '')
if (!items.length) {
ctx.toast({ title: 'Not a color', message: 'Try #22d3ee, rgb(34 211 238) or hsl(188 86% 53%)', style: 'failure' })
return false
}
return list({ title: items[0]!.title, items, filter: false })
},
},
{
name: 'random',
title: 'Random Color',
icon: 'palette',
keywords: ['color', 'colour', 'generate', 'random'],
view: () => {
const items = formats(randomHex())
return list({ title: items[0]!.title, items, filter: false })
},
},
],
sources: [
{
id: 'color',
title: 'Color',
debounce: 0,
position: 'top',
search: ({ query }) => formats(query),
},
],
})
import { defineExtension, type Context } from '@qikbar/qikbar'
const copy = (make: () => string, label: string) => async (ctx: Context) => {
const value = make()
await ctx.copy(value)
ctx.toast({ title: `Copied ${label}`, message: value.length > 40 ? `${value.slice(0, 40)}…` : value, style: 'success' })
}
const LOREM =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
function password(length: number) {
const chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*-_'
const bytes = crypto.getRandomValues(new Uint32Array(length))
return Array.from(bytes, (b) => chars[b % chars.length]).join('')
}
export default defineExtension({
name: 'generators',
title: 'Generators',
icon: 'wand',
description: 'UUIDs, passwords, timestamps and placeholder text.',
commands: [
{ name: 'uuid', title: 'Generate UUID', icon: 'hash', keywords: ['guid', 'id', 'random'], run: copy(() => crypto.randomUUID(), 'UUID') },
{
name: 'password',
title: 'Generate Password',
icon: 'key',
keywords: ['secret', 'random'],
arguments: [{ name: 'length', placeholder: 'Length (20)' }],
run: (ctx) => copy(() => password(Math.min(128, Math.max(8, Number(ctx.arguments.length) || 20))), 'password')(ctx),
},
{ name: 'timestamp', title: 'Copy Unix Timestamp', icon: 'clock', keywords: ['epoch', 'time', 'now'], run: copy(() => String(Math.floor(Date.now() / 1000)), 'timestamp') },
{ name: 'iso', title: 'Copy ISO Date', icon: 'calendar', keywords: ['date', 'now', 'time'], run: copy(() => new Date().toISOString(), 'ISO date') },
{ name: 'lorem', title: 'Copy Lorem Ipsum', icon: 'file-text', keywords: ['placeholder', 'text', 'dummy'], run: copy(() => LOREM, 'lorem ipsum') },
],
})
import { action, defineExtension, grid } from '@qikbar/qikbar'
const EMOJI: [string, string][] = [
['😀', 'grinning smile happy'], ['😂', 'joy tears laugh'], ['🥹', 'holding back tears'], ['😍', 'heart eyes love'], ['🤔', 'thinking hmm'],
['😎', 'cool sunglasses'], ['🥳', 'party celebrate'], ['😴', 'sleep tired'], ['🤯', 'mind blown'], ['😭', 'cry sob'],
['😅', 'sweat smile'], ['🙃', 'upside down'], ['😬', 'grimace awkward'], ['🫡', 'salute'], ['🤝', 'handshake deal'],
['👍', 'thumbs up yes ok'], ['👎', 'thumbs down no'], ['👏', 'clap applause'], ['🙌', 'raised hands hooray'], ['🙏', 'pray thanks please'],
['💪', 'muscle strong'], ['👀', 'eyes look'], ['🧠', 'brain smart'], ['❤️', 'heart love red'], ['🔥', 'fire hot lit'],
['✨', 'sparkles magic'], ['⭐', 'star'], ['⚡', 'zap lightning fast'], ['💯', 'hundred perfect'], ['✅', 'check done yes'],
['❌', 'cross no wrong'], ['⚠️', 'warning'], ['🚀', 'rocket launch ship'], ['🎉', 'tada party'], ['🎯', 'target goal'],
['💡', 'idea bulb'], ['📌', 'pin'], ['📎', 'paperclip attach'], ['📝', 'memo note write'], ['📅', 'calendar date'],
['📈', 'chart up growth'], ['📉', 'chart down'], ['💰', 'money bag'], ['💸', 'money flying'], ['🐛', 'bug'],
['🔧', 'wrench fix tool'], ['🔒', 'lock secure'], ['🔑', 'key'], ['🧪', 'test tube experiment'], ['🛠️', 'tools build'],
['📦', 'package box ship'], ['🗑️', 'trash delete'], ['🔍', 'search magnify'], ['💬', 'speech chat comment'], ['📣', 'megaphone announce'],
['☕', 'coffee'], ['🍕', 'pizza'], ['🌮', 'taco'], ['🍺', 'beer'], ['🎂', 'cake birthday'],
['🐶', 'dog puppy'], ['🐱', 'cat kitten'], ['🦄', 'unicorn'], ['🐙', 'octopus'], ['🦀', 'crab rust'],
['🌍', 'earth globe world'], ['🌙', 'moon night'], ['☀️', 'sun sunny'], ['🌈', 'rainbow'], ['❄️', 'snow cold'],
['⚽', 'soccer football'], ['🏀', 'basketball'], ['🎮', 'game controller'], ['🎵', 'music note'], ['🎨', 'art palette'],
['🏠', 'house home'], ['🏢', 'office building'], ['✈️', 'airplane travel'], ['🚗', 'car'], ['⏰', 'alarm clock time'],
]
export default defineExtension({
name: 'emoji',
title: 'Emoji',
icon: '😀',
description: 'Search and copy emoji.',
commands: [
{
name: 'search',
title: 'Search Emoji',
icon: '😀',
keywords: ['emoji', 'smiley', 'icon'],
view: grid({
title: 'Emoji',
placeholder: 'Search emoji…',
columns: 8,
items: EMOJI.map(([char, names]) => ({
title: names.split(' ')[0]!,
keywords: names.split(' '),
icon: char,
actions: [
action.copy(char, { title: 'Copy Emoji', shortcut: undefined }),
{
title: 'Paste into Search',
icon: 'search',
keepOpen: true,
run: (ctx) => {
ctx.setQuery(ctx.query + char)
return false
},
},
],
})),
}),
},
],
})
import { action, defineExtension, list, type Item } from '@qikbar/qikbar'
const CITIES: [string, string][] = [
['San Francisco', 'America/Los_Angeles'], ['New York', 'America/New_York'], ['São Paulo', 'America/Sao_Paulo'],
['London', 'Europe/London'], ['Berlin', 'Europe/Berlin'], ['Paris', 'Europe/Paris'], ['Lagos', 'Africa/Lagos'],
['Dubai', 'Asia/Dubai'], ['Mumbai', 'Asia/Kolkata'], ['Singapore', 'Asia/Singapore'], ['Shanghai', 'Asia/Shanghai'],
['Tokyo', 'Asia/Tokyo'], ['Sydney', 'Australia/Sydney'], ['Auckland', 'Pacific/Auckland'],
]
function clocks(): Item[] {
const now = new Date()
return CITIES.map(([city, tz]) => {
const time = new Intl.DateTimeFormat(undefined, { timeZone: tz, hour: 'numeric', minute: '2-digit' }).format(now)
const day = new Intl.DateTimeFormat(undefined, { timeZone: tz, weekday: 'short' }).format(now)
const offset = (Date.parse(now.toLocaleString('en-US', { timeZone: tz })) - Date.parse(now.toLocaleString('en-US'))) / 3.6e6
const diff = Math.round(offset * 2) / 2
return {
title: city,
subtitle: tz.replace(/_/g, ' '),
icon: { icon: 'clock', color: Number(time.split(':')[0]) >= 7 && Number(time.split(':')[0]) < 19 ? '#f59e0b' : '#6366f1' },
keywords: [tz],
accessories: [{ text: diff === 0 ? 'Local' : `${diff > 0 ? '+' : ''}${diff}h` }, { tag: `${day} ${time}` }],
actions: [action.copy(`${city}: ${day} ${time}`, { title: 'Copy Time' })],
}
})
}
export default defineExtension({
name: 'world-clock',
title: 'World Clock',
icon: 'clock',
description: 'The current time in cities around the world.',
commands: [
{
name: 'list',
title: 'World Clock',
icon: 'globe',
keywords: ['time', 'timezone', 'tz'],
view: () => list({ title: 'World Clock', placeholder: 'Search cities…', items: clocks() }),
},
],
sources: [
{
id: 'time-in',
title: 'World Clock',
debounce: 0,
position: 'top',
search: ({ query }) => {
const m = query.match(/^(?:time in|what time is it in)\s+(.+)$/i)
if (!m) return []
const q = m[1]!.toLowerCase()
return clocks().filter((c) => c.title.toLowerCase().includes(q) || c.subtitle!.toLowerCase().includes(q))
},
},
],
})