Omarchy App Launcher

block

The apps menu: type to filter, arrow keys to move, Enter to launch.

bunx --bun shadcn@latest add yamz8/omarchy-shadcn-registry/omarchy-app-launcher

Preview

7 of 7

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>
  )
}