A keyboard launcher for macOS and Linux. One hotkey, one search box over your apps, bookmarks, clipboard history, emoji, windows and a calculator. Every palette is a TypeScript extension against one API, so your own live next to the bundled ones.
Downloads will be on the Releases page:
a dmg for Apple silicon and Intel Macs, an AppImage and a deb for Linux.
The builds there today are the previous pal's (v0.1.x to v0.2.1, a
different program); this pal's first release is coming and is not
published yet. Until then, build from source.
The macOS builds are ad-hoc signed, so Gatekeeper refuses the first
launch once; Getting started has the
three ways past it.
- One search over everything. Apps, windows, files, bookmarks, clipboard history, emoji, system commands and every installed palette answer one query, ranked by what you pick; a calculation, a colour, a path or an address is answered inline, and a query nothing matches falls back to the web, a quicklink or a palette (Getting started).
- A hundred-odd palettes bundled in fifty-five extensions, from Applications and Clipboard History to GitHub, Slack, Gmail, Spotify, Hue, Docker, Obsidian, 1Password and a few games, each with its own actions, keys and settings (Palettes).
- Extensions in TypeScript, one directory with a manifest and an
index.ts: rows and actions, forms, a drawn view (a board, a picker, a dashboard), items on the menu bar, deep-link routes, several accounts of one extension (Extensions). The zero-code tier is a data file or a shell script (Scripts and data files). - A config file, not a settings database. One TOML file holds every setting, the Settings window writes it, a hand edit is picked up live, secrets go to the keychain, a theme file recolours every window (Config).
- Scriptable from outside. Every action is a
palcommand and apal://link (pal open emoji/emoji -q smile,pal pickas a picker for a script,pal://timer/start?duration=25m), from a shell, a keybind, a browser or a Shortcuts action (CLI, Links).
Every palette in pal is an extension, the bundled ones included: a
directory under extensions/ with a pal.json and an index.ts that
runs inside one long-lived Bun process. More live at
pal.cagdas.io/extensions and in the
Store palette; pal install <name> or pal install github:user/repo
puts one in the store directory, and general.extension_dirs loads a set
kept in dotfiles. Writing one is two files and a pal install .; the
walkthrough is in Extensions, the
smallest complete example in examples/hello-extension/, and the API is
@zcag/pal in sdk/.
Prerequisites: Rust (stable), Node 20+, Bun 1.4 or newer (the pinned
release in app/scripts/fetch-bun.sh; a 1.3 bun install rewrites every
bun.lock in the tree, so an older bun leaves the checkout dirty), and the
Tauri v2 system prerequisites
for your platform (Xcode command line tools and cmake on macOS, brew install cmake; webkit2gtk-4.1, gtk3, librsvg, openssl, base-devel on
Linux).
git clone git@github.com:zcag/pal.git && cd pal
(cd app && npm install)
bun install
for d in extensions/*/; do
[ -f "$d/package.json" ] && (cd "$d" && bun install)
done
cd app && npm run tauri devThe first cargo build fetches the pinned Bun release into
app/src-tauri/binaries/pal-bun-<triple> (app/scripts/fetch-bun.sh,
checksum verified, gitignored): it ships inside the app as the extension
host's runtime, and in dev the app runs that same copy. On macOS it also
builds the MediaRemote adapter into app/src-tauri/mediaremote/
(app/scripts/fetch-mediaremote.sh: a pinned clone and a cmake build,
about ten seconds; NOTICES.md), the system-wide Now Playing source the
media palette reads. In dev the host and the extensions load from the
repo and reload when a file changes. bun install at the root links the
workspace (host/, sdk/) and the @zcag/pal name the extensions
import.
A release build:
cd app && npm run tauri buildbeforeBuildCommand builds the UI and stages the host, the SDK and the
extensions under app/src-tauri/resources/ (app/scripts/build-extensions.sh:
each extension bundled to one index.js with bun build, the SDK inlined,
so no node_modules ships). The bundle also signs the updater artifacts,
so it wants TAURI_SIGNING_PRIVATE_KEY in the environment; without the
key, add -- --config '{"bundle":{"createUpdaterArtifacts":false}}'
(releases come from CI anyway: Releasing). Output
under target/release/bundle/:
- macOS:
macos/pal.appanddmg/pal_0.1.0_aarch64.dmg, ad-hoc signed (bundle.macOS.signingIdentity: "-").make appbuilds and installs it to/Applications, signed with a localpal-devidentity when the keychain has one, which keeps the Accessibility grant across rebuilds. - Linux:
appimage/pal_0.1.0_amd64.AppImage,deb/pal_0.1.0_amd64.deband an rpm. The first build downloadslinuxdeployand its plugins into~/.cache/tauri/. On a distro with current binutils (Arch) run it asNO_STRIP=true npm run tauri build: linuxdeploy's bundledstripcannot read the libraries and the bundle fails otherwise.
Inside the bundle, bun sits next to the pal binary (Contents/MacOS/,
usr/bin/) and the staged tree under the resource directory
(Contents/Resources/, usr/lib/pal/; on macOS Resources/mediaremote/
too). Installed extensions go in the store under the data dir
(~/Library/Application Support/pal/extensions/,
~/.local/share/pal/extensions/).
The layout of the repo:
app/the Tauri v2 shell: React UI insrc/, Rust insrc-tauri/core/pal-core, the parts that are neither UI nor OS glue (config, index, clipboard, icons)host/the extension host, one long-lived Bun process the app talks to over stdiosdk/@zcag/pal, the extension API: what an extension importsextensions/the bundled extensions, one directory eachexamples/the smallest complete extension, two script commands, two theme filesdocs/what pal.cagdas.io/docs rendersnotes/decisions and platform notes
Issues and pull requests are welcome at
github.com/zcag/pal. The checks CI runs on
every push (.github/workflows/ci.yml, macOS and Ubuntu): cargo clippy --all-targets from the repo root, npx tsc --noEmit in app/, bunx tsc --noEmit in host/ (which covers sdk/, extensions/ and examples/)
and in sdk/, and make test (the Rust workspace, the app's vitest, the
host's bun test). Docs are linted with npx markdownlint-cli2 "docs/**/*.md" README.md. An extension of your own does not need a pull request against
the app: publish it on GitHub and anyone can pal install github:you/repo; the store lists community extensions from a
community.json at this repo's root (an array of github: specs; none
listed yet), which is a one-line pull request.
- Getting started: install to the first extension, in order
- Config: the config file key by key
- Palettes: every bundled palette, its keys and settings
- Scripts and data files: the zero-code tier
- Keyboard: the keyboard grammar
- CLI:
paland its subcommands - Links:
pal://links and theirpaltwins - Extensions: writing a palette in TypeScript
- Troubleshooting: the log, permissions, the hotkey, PATH, the host
- Releasing: cutting a release, the updater
MIT, see LICENSE. What pal ships that is not its own (the Bun runtime, the Nerd Fonts symbols, the MediaRemote adapter) is listed with its licence in NOTICES.md.