Omarchy App Launcher
blockThe apps menu: type to filter, arrow keys to move, Enter to launch.
bunx --bun shadcn@latest add yamz8/omarchy-shadcn-registry/omarchy-app-launcherPreview
Dependencies
- npm
- lucide-react
- registry
- card, empty, input, yamz8/omarchy-shadcn-registry/omarchy-launcher-item, yamz8/omarchy-shadcn-registry/omarchy-keycap
Source
registry/blocks/omarchy-app-launcher.tsx
"use client"
import * as React from "react"
import {
CalendarIcon,
FolderIcon,
GlobeIcon,
MessageSquareIcon,
MusicIcon,
PencilIcon,
TerminalIcon,
} from "lucide-react"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/components/ui/empty"
import { Input } from "@/components/ui/input"
import { OmarchyLauncherItem } from "@/components/ui/omarchy-launcher-item"
import { OmarchyShortcut } from "@/components/ui/omarchy-keycap"
type LauncherEntry = {
id: string
name: string
description: string
icon: React.ReactNode
keys: readonly string[]
}
const entries: readonly LauncherEntry[] = [
{
id: "terminal",
name: "Terminal",
description: "Alacritty",
icon: <TerminalIcon />,
keys: ["Super", "Return"],
},
{
id: "browser",
name: "Browser",
description: "Default web browser",
icon: <GlobeIcon />,
keys: ["Super", "Shift", "Return"],
},
{
id: "files",
name: "File manager",
description: "Nautilus",
icon: <FolderIcon />,
keys: ["Super", "Shift", "F"],
},
{
id: "editor",
name: "Editor",
description: "Neovim",
icon: <PencilIcon />,
keys: ["Super", "Shift", "N"],
},
{
id: "music",
name: "Music",
description: "Spotify",
icon: <MusicIcon />,
keys: ["Super", "Shift", "M"],
},
{
id: "signal",
name: "Signal",
description: "Messaging",
icon: <MessageSquareIcon />,
keys: ["Super", "Shift", "G"],
},
{
id: "calendar",
name: "Calendar",
description: "Web app",
icon: <CalendarIcon />,
keys: ["Super", "Shift", "C"],
},
]
/**
* The Omarchy apps menu: type to filter, arrow keys to move, Enter to launch.
* Selection is reported through `onLaunch` — nothing is executed here.
*/
export function OmarchyAppLauncher({
onLaunch,
}: {
onLaunch?: (id: string) => void
}) {
const [query, setQuery] = React.useState("")
const [activeIndex, setActiveIndex] = React.useState(0)
const listId = React.useId()
const results = React.useMemo(() => {
const needle = query.trim().toLowerCase()
if (!needle) {
return entries
}
return entries.filter((entry) =>
`${entry.name} ${entry.description}`.toLowerCase().includes(needle)
)
}, [query])
// Keep the highlight inside the filtered list as it shrinks.
const selectedIndex = Math.min(activeIndex, Math.max(results.length - 1, 0))
function handleKeyDown(event: React.KeyboardEvent) {
if (results.length === 0) {
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
setActiveIndex((selectedIndex + 1) % results.length)
} else if (event.key === "ArrowUp") {
event.preventDefault()
setActiveIndex((selectedIndex - 1 + results.length) % results.length)
} else if (event.key === "Enter") {
event.preventDefault()
onLaunch?.(results[selectedIndex].id)
}
}
return (
<Card className="w-full max-w-md gap-0 overflow-hidden py-0">
<div className="border-b border-border p-2">
<Input
value={query}
onChange={(event) => {
setQuery(event.target.value)
setActiveIndex(0)
}}
onKeyDown={handleKeyDown}
placeholder="Search applications"
aria-label="Search applications"
aria-controls={listId}
aria-activedescendant={
results.length > 0 ? `${listId}-${selectedIndex}` : undefined
}
className="border-0 bg-transparent focus-visible:ring-0"
/>
</div>
<CardContent className="p-1">
{results.length === 0 ? (
<Empty className="py-8">
<EmptyHeader>
<EmptyTitle>No applications</EmptyTitle>
<EmptyDescription>Nothing matches “{query}”.</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<ul id={listId} role="listbox" aria-label="Applications">
{results.map((entry, index) => (
<li key={entry.id} role="none">
<OmarchyLauncherItem
id={`${listId}-${index}`}
role="option"
aria-selected={index === selectedIndex}
name={entry.name}
description={entry.description}
icon={entry.icon}
keys={entry.keys}
selected={index === selectedIndex}
onPointerMove={() => setActiveIndex(index)}
onClick={() => onLaunch?.(entry.id)}
/>
</li>
))}
</ul>
)}
</CardContent>
<CardFooter className="justify-between gap-4 border-t border-border py-2">
<span className="text-xs text-muted-foreground">
{results.length} of {entries.length}
</span>
<OmarchyShortcut keys={["Super", "Alt", "Space"]} />
</CardFooter>
</Card>
)
}