Palettes
The palettes of pal's own extensions. Each is an extension with a
pal.json manifest, in zcag/pal-extensions
(the games in zcag/pal-games); a
path below such as calc/index.ts or test/calc.test.ts is in that
repo; its settings go under [extensions.<name>] in
the config file unless noted, and the per-palette keys (enabled, alias,
hotkey, icon) go under [palettes.<id>]. See Config.
Three words used below. An input palette is never in the index: its
rows come from the extension on every keystroke inside it, and the root
only has the palette's own row. A live palette is listed again every
time the panel shows (not twice within 2 s, and with a ttl only once its
last listing is older than that), so its rows are current at the root. A
palette that is neither is indexed: listed once, searched from the index,
refreshed with ⌘R; a scripts palette keeps its listing across restarts
for the extension's ttl (an hour by default) unless it sets its own.
At the root every palette also has a tier
(Extensions): primary for
what is reached by name (Applications, Windows, Menu Bar Items, Bookmarks,
Quicklinks, Snippets, Recent Files, Browser Tabs, System, SSH Hosts), ranked up
and capped at 8 rows per palette; catalog for the big static lists (Emoji,
Unicode Characters, Colors, both Icons palettes, a data file of 100 rows or
more), ranked down and capped at 3, the rest behind a "12 more in Emoji" row;
normal for everything else, capped at 6. [palettes.<id>] tier overrides it.
Every palette that ships, by extension. The id is the config key
([palettes.<id>]) and the heading of its section; a multi extension's
second account gets the same palettes under <name>@<suffix>-<palette>
(Config).
| palette | id | kind, tier | what Enter does |
|---|---|---|---|
| 2048 | 2048 |
view, normal | New game (asks mid-game); Keep going after the first 2048 |
| Apple TV Remote | appletv-remote |
view, normal | Select (the clickpad's centre) |
| Now Playing on Apple TV | appletv-now |
view, normal | The panel's row (play the queue item, jump to the chapter, pick the track), else play or pause |
| Play on Apple TV | appletv-play |
input, normal | Play the link on the TV |
| Apple TV Apps | appletv-apps |
indexed, normal | Open the app on the TV |
| Apple TV | appletv-commands |
indexed, primary | Run the command on the TV |
| Apple TV Users | appletv-users |
indexed, normal | Switch to the user |
| Set Up Apple TV | appletv-setup |
view, normal | Pair with the Apple TV |
| Applications | apps |
indexed, primary | Open the app |
| Audio | audio |
live, normal | Set as the default output or input |
| Battery & Power | power |
view, normal | Find the process under the cursor in Processes |
| Blackjack | blackjack |
view (surface), normal | Deal, stand, or the next hand; declines insurance |
| Bluetooth | bluetooth |
live, normal | Connect or disconnect |
| Bookmarks | bookmarks |
indexed, primary | Open in the browser |
| Browser History | bookmarks-history |
input | Open in the browser it came from |
| Browser Tabs | browser-tabs-tabs |
live, primary | Switch to the tab |
| Calculator | calc |
input, normal | Copy the result |
| Camera, Mic & Screen | privacy |
live, normal | Bring the app forward |
| My Schedule | calendar-schedule |
live, normal | Join the call, else open in Calendar |
| Today | calendar-today |
live, normal | Join the call, else open in Calendar |
| Quick Add Event | calendar-quick |
input | Add the typed line as an event |
| Clipboard | clipboard-rows |
input, normal | What the row is for: open, call, paste as plain, copy the answer |
| Clipboard History | clipboard-history |
input, normal | Paste into the app in front |
| Colour Picker | colors-picker |
view, normal | |
| Named Colours | colors |
indexed, grid, catalog | Open in Picker |
| Colour History | colors-history |
live, normal | Open in Picker (on the top row: pick from the screen) |
| Convert Colour | colors-convert |
input, normal | Open in Picker |
| Crossword | crossword |
view (surface), normal | Next clue; Next puzzle once solved |
| Diff | diff |
view, normal | Copy the unified diff |
| Diff from History | diff-pick |
input, normal | Pick the side (with two marked: diff them) |
| Disk Space | space |
live, normal | Open the map of the root (scans first when there is none) |
| Disk Map | space-map |
view, normal | Zoom into the focused folder; open a file |
| Largest Files | space-largest |
live, normal | Open the file |
| Largest Folders | space-folders |
live, normal | Show in the map |
| Cleanup Suggestions | space-cleanup |
live, normal | Show in the map |
| Docker Containers | docker |
live, normal | Stop a running container, start a stopped one |
| Docker Images | docker-images |
live, normal | Run, after a form for the name and ports |
| Compose Projects | docker-compose |
live, normal | Up |
| Downloads | downloads |
live, primary | Open the file |
| Emoji | emoji |
indexed, grid, catalog | Copy emoji |
| Files | files |
input, normal | Open the file, browse a folder |
| Browse Folder | files-browse |
input, normal | Browse a folder, open a file |
| Finder Selection | files-selection |
input, normal | Open the file, browse a folder |
| Recent Files | files-recent |
live, primary | Open the file |
| Flashcards | flashcards |
view (surface), normal | Show the meaning, then Knew it |
| Flashcard Packs | flashcard-packs |
live, normal | Study the pack |
| Add Flashcard | flashcard-add |
input, normal | Add the card to My cards |
| Browse Anki Decks | flashcard-anki |
input, normal | Add the deck to Flashcards (added: study it) |
| Games | games |
live, normal | Play the game |
| Generate | generate |
input, normal | Copy the value (show the QR code on its row) |
| GIFs | gifs |
indexed, grid, normal | Copy the GIF file |
| Favourite GIFs | gifs-favourites |
indexed, grid, normal | Copy the GIF file |
| Pull Requests | github-prs |
indexed, normal | Open the pull request |
| Issues | github-issues |
indexed, normal | Open the issue |
| Repositories | github-repos |
indexed, normal | Open on GitHub |
| Notifications | github-notifications |
live, normal | Mark read and open |
| Search GitHub | github-search |
input, normal | Open what was found |
| Inbox | gmail-inbox |
live, normal | Open the message in Gmail |
| Search Mail | gmail-search |
input, normal | Open the message in Gmail |
| Labels | gmail-labels |
indexed, catalog | Open the label in Gmail |
| Compose | gmail-compose |
indexed, normal | Open the form; on the form, send |
| Drafts | gmail-drafts |
live, normal | Open the draft in Gmail |
| Google Search | google |
input, normal | Search Google (on a result row: open it) |
| Home Assistant | home-assistant-entities |
live, normal | The domain's first action: toggle, activate, run, copy value |
| Home Assistant Services | home-assistant-services |
indexed, normal | Open the form, then call |
| Home Assistant Areas | home-assistant-areas |
indexed, normal | List the area's entities |
| Hue Rooms | hue-rooms |
live, primary | Toggle the room |
| Hue Lights | hue-lights |
live, normal | Toggle the light |
| Hue Scenes | hue-scenes |
live, primary | Play the scene |
| Hue Light | hue-light |
view, normal | Toggle, or apply the chosen preset, scene or effect |
| Set up Hue | hue-setup |
view, normal | Pair with a bridge |
| Hue Sensors | hue-sensors |
live, normal | Copy the reading |
| Hue Automations | hue-automations |
live, normal | Enable or disable |
| Hue Entertainment | hue-entertainment |
live, normal | Start streaming |
| Nerd Font icons | icons |
indexed, grid, catalog | Copy glyph |
| Freedesktop icon names | icons-freedesktop |
indexed, grid, catalog | Copy name |
| Iconify Icons | icons-iconify |
input, grid | Copy SVG |
| Images | images |
input, normal | Compress |
| Immich | immich |
input, grid | Open the photo in Immich |
| Immich Albums | immich-albums |
indexed, normal | Open the album as a grid |
| Immich People | immich-people |
indexed, normal | The person's photos as a grid |
| On This Day | immich-memories |
live, normal | That year's photos as a grid |
| Makefile Targets | make |
indexed, normal | Run the target |
| Maps | maps |
input, normal | Open the place or the route |
| Now Playing | media |
live, normal | Play or pause |
| Menu Bar Items | menu-bar |
live, primary | Press the menu item |
| Minesweeper | minesweeper |
view (surface), normal | Open the cell (around a satisfied number); New game once over |
| Night Parade | night-parade |
view (surface), normal | Pause or carry on (in the page, Enter pauses mid-night and picks on the menus and cards) |
| Network | network |
live, normal | Copy the value |
| Notes | obsidian-notes |
indexed, primary | Open in Obsidian (or the editor, per the setting) |
| Search Notes | obsidian-search |
input, normal | Open in Obsidian (or the editor, per the setting) |
| Daily Notes | obsidian-daily |
live, normal | Open the note; on Create today's note, create it (asks first); on the forms, open them |
| Tags | obsidian-tags |
indexed, catalog | The notes with the tag |
| Recent Notes | obsidian-recent |
live, normal | Open in Obsidian (or the editor, per the setting) |
| Backlinks | obsidian-backlinks |
indexed, normal | Open the linking note |
| Outgoing Links | obsidian-outgoing |
indexed, normal | Open the linked note; on a link to nothing, create it |
| 1Password | onepassword-items |
indexed, normal | Copy the password |
| Verification Codes | otp |
live, normal | Paste the code |
| Processes | processes |
input, normal | Kill (asks first) |
| Quicklinks | quicklinks |
indexed, primary | Open, or fill in the {query} |
| Screenshots | screenshots |
live, primary | Capture, or open the screenshot |
| Services | services |
live, normal | Stop or start (unload or load on macOS) |
| Sessions | sessions |
live, primary | Focus the session's terminal (an ended one: resume it in a new terminal) |
| Shell | shell |
input, normal | Run the command (in the view: copy the output) |
| Shell History | shell-history |
input, normal | Run the command again |
| Shortcuts | shortcuts |
indexed, primary | Run the shortcut |
| Unreads | slack-unreads |
live, normal | Open the conversation in Slack |
| Channels | slack-channels |
indexed, catalog | Open in Slack |
| Search Slack | slack-search |
input, normal | Open the message in Slack |
| Status | slack-status |
live, normal | Set it |
| Snippets | snippets |
indexed, primary | Paste into the app in front |
| Solitaire | solitaire |
view (surface), normal | Draw (in the page, Enter picks up or drops the cards; twice on one card, sends it where it goes) |
| Speedtest | speedtest |
view, normal | Start the test (stop it while it runs) |
| Speedtest History | speedtest-history |
live, normal | Copy the run (on Trend: show the bars) |
| Lyrics | spotify-now-playing |
view, normal | Play or pause |
| Search Spotify | spotify-search |
input, normal | Play |
| Playlists | spotify-playlists |
indexed, normal | Play |
| Library | spotify-library |
indexed, normal | Play |
| Spotify Devices | spotify-devices |
live, normal | Play here (a device); volume up, down, mute (the volume rows) |
| Queue | spotify-queue |
live, normal | Skip to the row (play or pause on the first) |
| Spotify | spotify-commands |
indexed, primary | Run |
| SSH Hosts | ssh |
indexed, primary | Connect in a terminal |
| Store | store |
input, primary | Install, update, or open the store page |
| Sudoku | sudoku |
view (surface), normal | Place the digit (in the page); the next puzzle once solved |
| System | system |
live, primary | Run the command |
| Search tela | tela-search |
input, normal | Open the page in tela |
| Ask tela | tela-research |
input, normal | Ask; on a source, open it in tela |
| Pages | tela-pages |
indexed, normal | Open the page in tela |
| Spaces | tela-spaces |
indexed, normal | The space's pages |
| New Page | tela-new-page |
indexed, normal | The form |
| Decks | tela-decks |
indexed, normal | Open the deck in tela |
| Sheets | tela-sheets |
indexed, normal | Open the sheet in tela |
| Comments | tela-comments |
live, normal | Mark read and open the page |
| Backlinks | tela-backlinks |
indexed, normal | Open the linking page in tela |
| Timers | timer-timers |
live, normal | Pause, resume or dismiss |
| Translate | translate |
input, normal | Copy the translation (on Swap: translate it back) |
| Translation History | translate-history |
live, normal | Copy the translation |
| Turkish | turkish |
input, normal | Paste the converted text (over the selection it came from) |
| Typing | typing |
view (surface), normal | A new test (Tab) |
| Unicode Characters | unicode |
indexed, grid, catalog | Copy character |
| Vortex | vortex |
view (surface), normal | Play, or play again (in the page, Space) |
| Chats | whatsapp-chats |
live, primary | Open the chat (a group opens WhatsApp at the top) |
| Unread | whatsapp-unread |
live, normal | Open the chat |
| Search WhatsApp | whatsapp-search |
input, normal | Open the chat |
| Contacts | whatsapp-contacts |
indexed, catalog | Open a chat with the contact |
| Wi-Fi | wifi |
live, normal | Join, scan, or turn the radio off or on |
| Window Management | window-management |
indexed, normal | Apply to the focused window |
| Arrange Window | window-management-arrange |
input, normal | Pick the window, then its layout |
| Windows | windows |
live, primary | Focus the window |
| Spaces | windows-spaces |
live, normal | Bring the space in front |
| Wordle | wordle |
view, normal | Submit the guess |
| Yahtzee | yahtzee |
view (surface), normal | Hold the die, or score the category under the cursor; the first roll of a round |
| YouTube | youtube-search |
input, normal | Open in the browser |
| YouTube Channels | youtube-channels |
input, normal | Latest videos |
| Watch Later | youtube-later |
live, normal | Open in the browser |
| Scripts and data files | scripts-<name> |
as configured | as configured |
| Script Commands | scripts-commands |
live, normal | runs the script command as its header says |
Applications (apps)
Installed applications with their own icons.
- macOS:
.appbundles in/Applications,/System/Applicationsand~/Applications, one level deep so the Utilities folders come along. A row from~/Applicationsor an extra folder has the folder as its subtitle; the two usual roots say nothing. The details (⌘I) lead with the folder over the name, running and the version as chips. The bundle id is a keyword, socom.apple.oranthropicfinds the app. Enter opens the bundle path with the system opener. - Linux:
.desktopentries from every XDG data dir (~/.local/share,$XDG_DATA_DIRS, the flatpak exports), first directory wins per desktop id. Entries withNoDisplay,Hidden, aTryExecthat is not installed, or anOnlyShowIn/NotShowInthat excludes$XDG_CURRENT_DESKTOPare skipped.Comment(elseGenericName) is the subtitle;GenericName,Keywords, the binary and the desktop id are keywords. Enter launches throughgio launch, elsegtk-launch, else the parsedExec.Terminal=trueentries run in$TERMINAL, else the first of kitty, foot, xterm found on PATH.
Running apps, System Settings panes and actions
- A running app carries a green
runningtag (as of the last listing: the palette is indexed, so the tag is refreshed by⌘R, a settings change, and after a Quit or Hide from the panel) and its actions are Open, Quit (⌘Q), Hide (⌘H), Reveal in Finder (⌘⇧R), Copy path (⌘C), Copy bundle id (⌘⇧C); an app that is not running has Quit and Hide last, and both answer "is not running" as a toast rather than launching it. Quit asks the app through AppleScript (so it can ask you to save; a save dialog left up is the app's to finish) and falls back toSIGTERMfor a bundle Launch Services does not know. Hide goes through System Events (an Automation prompt for pal once). - Keywords: the bundle id as before, plus
CFBundleDisplayNameandCFBundleNamewhen they differ from the folder name (Chromefinds Google Chrome). Localised names (InfoPlist.strings) are not read: they are binary plists in most system apps and would cost a spawn per app. - System Settings panes are rows: 35 common panes (Keyboard,
Displays, Privacy & Security, Wi-Fi, ...) with the System Settings icon,
System Settingsas subtitle,settings/preferencesand a few words per pane as keywords. Enter opens the pane (x-apple.systempreferences:<id>),⌘Ccopies that url. A curated table (macOS 13+ ids), not a scan of/System/Library/ExtensionKit: the extensions there mix panes with intents and widgets and carry no display names. - Linux: a
.desktopfile's[Desktop Action …]groups ("New Window", "New Private Window") are the row's secondary actions, run from their ownExec(there is no launcher CLI for an action); Copy path too. The parser isapps/desktop.ts.
Settings, [extensions.apps]:
| key | type | default | what |
|---|---|---|---|
folders |
list of paths | [] |
Extra folders scanned in addition to the system's application folders. ~ is expanded. A change rescans. |
Blackjack (blackjack)
A hand of blackjack on a felt table. Enter on the palette's row (or its
hotkey) opens the table as a view level: a surface, the extension's own
page, with the Kenney deck dealt from a shoe in the corner. The title line
shows the phase ("Place your bet", "Your turn", "Dealer busts"), the
footer the primary move, and ⌘K lists every legal move with its key.
- Betting:
↑/+and↓/-move the bet by the minimum (or click the chips), Enter deals: player, dealer, player, then the hole card face down. - Playing:
↑orHhit,↓orSstand,→orDdouble down (first two cards, one card then stand),←orPsplit (a pair, once; split aces take one card each; the hands slide apart). Enter stands too. Totals show on a badge over each hand ("Soft 17"); on stand the hole card turns over and the dealer draws to 17. - Settled: the result and the net for the hand; a win glows, a bust
shakes, a blackjack gets its own flourish, and the chips go to the
dealer or come back with the winnings. Enter for the next hand.
Nstarts a new game (asks first) with a fresh bankroll and record. - Every move is a button on the rail under the felt too, with its keys on it; the arrows and Enter play a whole hand one-handed.
- Escape leaves at any point; the hand, the bankroll and the record persist (in the extension's storage), so the table is as you left it next time.
Rules: dealer stands on 17 (soft 17 too unless dealer_hits_soft_17),
blackjack pays 3:2, a dealer blackjack is checked at once, doubling after a
split is allowed, no surrender. The shoe is decks decks and is
reshuffled before a deal once under a quarter of it is left (the bar on
the shoe). Insurance is offered on an ace only with insurance = true
(↑ or i takes it; ↓ or Enter declines), costs half the bet and pays
2:1.
The page plays with the extension's own rules (game.ts, the file the
host tests), so the table and the tests cannot disagree; the surface node
it rides on is in Extensions.
[extensions.blackjack]
decks = 6 # 1..8
starting_bankroll = 1000
min_bet = 10 # also the step for + and -
dealer_hits_soft_17 = false
insurance = false
Bookmarks (bookmarks, bookmarks-history)
Hand-picked links from a JSON file: a JSON array of objects with name and
url, plus optional subtitle (the address when absent), icon (a glyph,
emoji or hex colour; a row with a url and no icon gets the site's favicon)
and keywords (a list of strings); the previous pal's bookmarks file
reads as is.
[
{ "name": "Home Assistant", "url": "http://ha.lan", "keywords": ["ha", "home"] },
{ "name": "GitHub", "url": "https://github.com", "icon": "🐙" }
]
Actions:
| action | shortcut | what |
|---|---|---|
| Open in browser | Enter |
opens the url; over marked rows (⇧↓, ⌘-click), every one in a tab |
| Copy link | ⌘C |
copies the url |
Browser bookmarks
The browsers' bookmarks join the JSON file: Chrome, Brave, Edge,
Chromium, Vivaldi and Arc (every profile's Bookmarks JSON, the profile
name from Local State when there is more than one), Safari
(~/Library/Safari/Bookmarks.plist, read through plutil -convert xml1
since the JSON form refuses the Reading List's dates; the Reading List
itself is left out) and Firefox (every profile's places.sqlite, copied
first because the running browser holds it locked, then bun:sqlite;
tags and place: queries left out). Every source is read on every list.
- The file's rows come first, then each browser in the
browserssetting's order; a url two sources have is listed once, the first wins. Browser rows sit in a section per browser and profile (Chrome,Chrome (Work),Safari,Firefox), carry the folder path as an accessory (Bookmarks Bar / Dev) and as keywords, and get the site's favicon. A bookmark saved without a name (Chrome's bar keeps those) is named by its bare address (calendar.google.com) and says nothing twice under it. - Actions: Open in browser (
Enter), Copy link (⌘C), Copy as markdown (⌘⇧C,[name](url)), and on a browser row Open in Chrome/Safari/... (open -aon macOS, the browser's binary on Linux). - Safari's file needs Full Disk Access: without it the Safari section is one inert row saying so (System Settings > Privacy & Security > Full Disk Access, add pal).
Settings, [extensions.bookmarks], in addition to file:
| key | type | default | what |
|---|---|---|---|
browsers |
list | ["chrome", "brave", "edge", "chromium", "vivaldi", "arc", "safari", "firefox"] |
Whose bookmarks and history to list, in order. A browser with no profile on the machine lists nothing. [] is the file alone, and no history. |
exclude_folders |
list | [] |
Bookmark folders skipped, by name (Archive) or a short path (Bookmarks Bar/Old), case-insensitive. |
Settings, [extensions.bookmarks]:
| key | type | default | what |
|---|---|---|---|
file |
path | ~/.config/pal/data/bookmarks.json |
The bookmarks file. ~ is expanded. |
Browser History (bookmarks-history)
A second palette of the extension, input rather than indexed: every
keystroke searches the visit history the same browsers keep (the
browsers setting; Safari's History.db is behind Full Disk Access and
is not read), by title and address, newest first, fifty rows at most, a
url once. Chrome, Brave, Edge, Chromium, Vivaldi and Arc are every
profile's History (SQLite, the urls table; Chrome's clock counts
microseconds from 1601), Firefox every profile's places.sqlite
(moz_places, microseconds from 1970). A running browser holds its file
locked, so each is copied under pal's cache (~/Library/Caches/pal/bookmarks,
~/.cache/pal/bookmarks on Linux) before it is read; the copy is taken
again when the file's mtime moved, at most every 30 seconds, so a
keystroke never copies a large history twice. Hidden entries (a redirect
Chrome keeps for autocomplete) and pages never visited are left out; no
browser with a history is one inert row saying which are read.
- The row: the page's title (the address when it has none), the address
under it and as the favicon's source, when it was last visited on the
right, the browser and profile as the section (
Chrome (Work)). - Actions: Open in Chrome/Firefox/... (
Enter, the browser it came from:open -aon macOS, the browser's binary on Linux), Copy link (⌘C), Open in default browser (⌘O).
Calculator (calc)
An input palette: what you type is answered on every keystroke. Three
parsers take the query in turn: dates and time zones, currencies, then
mathjs for arithmetic and units. The result is the
row's title, the query as it was understood is the subtitle (12 USD to TRY), and a second row carries another form when there is one: the other
bases of an integer, a fraction, the reverse conversion, the ISO date. A
query that does not parse lists nothing, so nothing flashes while typing;
with nothing typed the palette shows three hint rows.
Actions on every result row: Enter copies the result as shown, ⌘Enter
pastes it into the app in front, ⌘⇧C copies it without grouping or
symbols (583.68, 1000000), ⌘⇧E copies expression = result.
What works (every line below is checked by test/calc.test.ts in pal-extensions):
| kind | expression | result |
|---|---|---|
| arithmetic | 2+2, 2^10, 5!, 1e6, 2^100 |
4, 1,024, 120, 1,000,000, 1.2676506e+30 |
| functions | sqrt 2, sqrt(16), square root of 625, 2 power 10, 3 x 4, 10 mod 3, pi, e |
1.414213562, 4, 25, 1,024, 12, 1, 3.141592654, 2.718281828 |
| percent | 15% of 240, 20% off 80, 240 + 15%, 240 - 15%, 50% |
36, 64, 276, 204, 0.5 |
| bases | 0xff, 0b1010, 255 to hex, 255 in binary, hex(255) |
255 (+ a 0b11111111 row), 10, 0xff, 0b11111111, 0xff |
| fractions | 1/3, 0.375 |
0.3333333333 + a 1/3 row, 0.375 + a 3/8 row |
| units | 5 km to miles, 72 f to c, 212 °F to °C, 300 k to c, 12 gb to mb, 1 gib to mb, 3 weeks to days, 100 kph to mph, 1 acre in m2, 5 kg to lb, 10 ft in m, 1 hp to kw, 5 ft 3 in to cm, 2 hours + 30 minutes to minutes |
3.10686 miles, 22.2222 °C, 100 °C, 26.85 °C, 12,000 MB, 1,073.74 MB, 21 days, 62.1371 mph, 4,046.86 m2, 11.0231 lb, 3.048 m, 0.7457 kW, 160.02 cm, 150 minutes |
| currency | 12 usd to try, 12 usd in eur, €12 to $, 12 dollars in lira, £10 to ₺, 1k usd, 12*2 usd to try, usd try |
583.68 TRY, 10.40 EUR, 13.85 USD, 583.68 TRY, 655.85 TRY, 48,640.26 TRY, 1,167.37 TRY, 48.64 TRY (at the test's canned rates) |
| thousands | 210k / 12, 2k + 500, 1.5m usd to try |
17,500, 2,500, 72,960,395.18 TRY; k is thousands anywhere, m and b millions and billions before a currency, since a bare 5m is metres |
| home currency | 12 usd, $12, 100 try |
to home_currency (12 USD to TRY); the home currency itself goes to USD |
| variables | salary_month, salary_month to eur, rent / salary_month, height to ft (with vars below) |
455,272.87 TRY (+ a 9,360.00 USD row), 8,111.62 EUR, 0.09225236807 with 9.225%, 6.00394 ft; the subtitle is the query with each name as its value (42,000.00 TRY / 9,360.00 USD) |
| how many fit | 1500 usd in salary_hour, 1500 usd in salary (a prefix), 5 km in lap |
27.78 salary_hour; one row per salary_* (27.78 hour, 3.472 day, 0.1603 month), largest count first; 100 pool, 12.5 track |
| dates | today, tomorrow, today + 3 days, 2026-10-14 + 45 days, 3 weeks from now, in 90 days, 2 days ago, 25 dec 2026, dec 25, 2026, 25 aralık, 3 şubat 2027 |
the date written out (Saturday, September 19, 2026), a relative accessory (in 3 days), an ISO row (2026-09-19) |
| weekdays | friday, next friday, last friday, this mon, next week, next month, gelecek cuma, geçen salı, next friday + 1 week |
the coming Friday (today on a Friday); next is the one after today, last the one before; next week/month a week or a month from today |
| now | now |
the time, with the date as accessory; ISO and unix rows |
| counts | days until 25 dec, days since 2026-08-31, days until friday, weeks until 25 dec, months since 2025-06-15, 2026-01-01 - 2025-06-15, 2025-06-15 to 2026-01-01, weeks between 2025-06-15 and 2026-01-01, between 1 mar and 14 oct |
100 days (with 14 weeks 2 days · 3 months 9 days), 16 days, 2 days, 14.3 weeks, 15 months, 200 days, 200 days, 28.6 weeks, 227 days |
| workdays | workdays until 25 dec, working days until friday, business days between 2026-10-01 and 2026-11-01, workdays since 2026-09-07 |
72 workdays (with 100 days · Mon to Fri, holidays not counted), 2 workdays, 22 workdays, 7 workdays: Monday to Friday from the first day (counted) up to the last (not) |
| weeks | what week is it, week number, what's the week number, week of 25 dec, week number of 2027-01-01, week 1 2027, week 42 |
Week 38, the subtitle this week: Mon 14 Sep to Sun 20 Sep, of 53 in 2026 on the right; rows for the ISO week (2026-W38) and its Monday. ISO weeks: 1 January 2027 is in 2026-W53 |
| day of a date | what day is 2027-01-01, what day was 29 oct 1923, day of week 25 dec |
Friday (the full date as subtitle), Monday, Friday |
| time zones | time in tokyo, tokyo time, what time is it in tokyo?, 3pm in tokyo, 3pm istanbul to new york, now in PST, 3pm tokyo, 23:00 utc to tokyo, 10:00 in india, 14:30 ist to cet, 5pm ldn in sf, 10am new york to utc+3, noon in london, time in caracas, time in londra, 10:00 in Europe/Berlin |
16:30 with 6 h ahead · Asia/Tokyo and the subtitle 10:30 Istanbul (GMT+3) → Tokyo (GMT+9); tomorrow, 08:00 or yesterday, 15:00 when the day there is not today's here; 2 h 30 min ahead for India; rows with the full date there, ISO with its offset, and unix |
| unix time | 1759000000, @1759000000, unix 1759000000, 1759000000 to date, 1759000000000, unix time, unix, 2026-01-01 12:00 to unix |
the moment written out (Saturday, September 27, 2025 at 22:06, + ISO and unix rows), the current unix time, 1767258000 (local time, here Istanbul) |
| durations | 3h20m + 45m, 2 hours + 30 minutes, 8h - 45 min - 30min, 90 min in hours, 1h30m to minutes |
4 h 5 min (with 4:05, a 245 minutes and a 4.0833 hours row), 2 h 30 min, 6 h 45 min, 1.5 hours, 90 minutes |
Details:
- Currencies are the 30 the ECB fixes (USD, EUR, GBP, TRY, JPY, CHF,
...); no crypto, the source has none. Symbols (
$ € £ ₺ ¥ ₹ ₩ ₪ zł,HK$,R$, ...), names (dollars,lira,euro,quid,yen,TL) and ISO codes in any case are understood;to,in,asand→join the two sides;1k,2.5mare thousands and millions; the amount may be an expression. A bareusdis the rate (1 USD to TRY). Results use the currency's own decimals (JPY none); the accessories carry the rate andrates <date>, the detail pane the rate, its inverse, both names, the date and the source. A target still being typed (12 usd to tr) answers for the source alone meanwhile; an unknown target lists nothing, a currency the set lacks is an inertNo rate for Xrow. - Rates come from the ECB reference rates through
https://api.frankfurter.dev/v1/latest(no key, one fix per working day), fetched on the first currency query, never before. The first query without a cache shows an inertFetching exchange rates…row, and the next keystroke has the numbers. The set is kept in the extension's storage with its date and fetch time, and refreshed in the background once it is a day old; meanwhile, and whenever the network is away, the cached set answers with its date on the row. A failed fetch is retried after a minute.PAL_CALC_OFFLINE=1in the host's environment disables fetching (the tests),PAL_CALC_RATES_URLpoints it elsewhere. - Units are mathjs's, plus the spellings people type:
gb/mb/kb(bytes; mathjs's ownkbis a kilobit),kph/kmh/mph,kw/kwh,mo/yr/hr/wk,ha,sqm,tsp/tbsp,lbs, andf/c/kas degrees on either side of ato.5 ft 3 inand5'3"add up. Unit and rate results show at most 6 significant digits whateverprecisionsays. - Precision is significant digits for a number below 1 and for the
fraction of one above; integers are never rounded (
123456789*1000is exact). Numbers past 1e15 or under 1e-6 go exponential. - Locale sets grouping and the decimal separator both ways: under
tr(orde),1,5 + 2is3,5and1.000.000 / 3is333.333,3333; underena1,000is a thousand. Dates and relative phrases follow it too. - Dates are ISO (
2026-12-25, with an optional12:00),25 dec,dec 25, 2026,25.12.2026,25 aralık(Turkish month names, with or without their letters), the wordsnow,today,tomorrow,yesterday, a weekday in English or Turkish (friday,cuma) with an optionalnext/last/this(gelecek/geçen/bu), andnext week,last month. Times are 24 h whatever the locale. The clock is the SDK's (PAL_NOWpins it in the tests). Arithmetic takesdays,weeks,months,years,hours,minutes(andd,w,mo,y,h,min), chained (today + 1 month - 2 days); months keep the day of month, clamped. Day counts are calendar days (DST-proof);A - Bis A minus B,A to Breads left to right. A date-shaped query that is no date (2026-02-30) lists nothing rather than falling through to arithmetic. - Zones are IANA ids in any case,
utc+3-style whole-hour offsets, the common abbreviations (utc,cet,est,pst,jst,aest, ...) and 120-odd city and country names (tokyo,new york,sf,ldn,india,hong kong).istis Istanbul here (the airport code), India isindia,delhi,mumbai; any IANA id's city also works (caracas,ho chi minh), and a few Turkish names (londra,moskova). Without a source zone the local one (the system's) is meant; the time is today's in the source zone.3pm in tokyois your 3pm in Tokyo,3pm tokyoTokyo's 3pm here. All from the runtime's time zone data, no network. - Unix time: a bare 10-digit number starting with 1 (2001 to 2033; 13
digits for milliseconds) is a unix time, so
1759000000at the root answers with its date;@reads any. Other numbers stay numbers (5321234567). - Durations add and subtract
d,h,m,s(and their spelled forms), compound or not (3h20m).mmeans minutes only beside an hour, so45m + 10mis still metres.
What does not:
- Crypto (
1 btc to usd): frankfurter carries no crypto; it lists nothing. - Currencies outside the ECB set (
12 usd to aed): nothing, or aNo raterow once the set is loaded. - A trailing operator or an open parenthesis (
1 +,(1+2): nothing until the expression closes. A trailing=is fine. - Variables do not persist between keystrokes:
x = 5shows5, but a laterxis undefined. 10x3without spaces (0xwould be hex); write10 x 3or10*3.- Half-hour offsets as
utc+5:30; use the zone (india). - Public holidays:
workdayscounts Monday to Friday only.
At the root, a query that reads as sums, a conversion, a date or a time
(2+2, 15% of 80, 12 usd to try, 5 km to miles, 3 days from now,
today + 3 days, next friday, time in tokyo, tokyo time, what week is it, unix time, 1759000000) is answered inline under a
Calculator section above the hits, with the same actions (Enter copies
the result). A bare number or a single word never wakes it (mon is on
the way to Monitor, friday answers only inside the palette), and a
phrase without a digit or a date word answers only when the date parser
reads it whole, so time machine and screen time stay app searches.
The check costs about 5 µs a keystroke. A query nothing matched gets an "Ask Calculator"
fallback row that opens the palette with it typed.
Settings, [extensions.calc]:
| key | type | default | what |
|---|---|---|---|
precision |
number, 2 to 20 | 10 |
Significant digits in a result; integers are never rounded, units and rates show at most 6. |
locale |
text | en |
How numbers and dates are written and how a typed number is read (en: 1,234.5; tr, de: 1.234,5). |
home_currency |
text | empty | What a bare amount (12 usd) converts to. Empty: the currency of the machine's time zone (Europe/Istanbul is TRY, Europe/* otherwise EUR, America/* USD, ...), then the locale's region, else USD. |
vars |
list | [] |
Variables, one name = value per line (salary_hour = 54 usd, salary_month = salary_hour * 2080 / 12, rent = 42000 try). A query naming one is expanded before it is read, so a value is anything calc reads and may use other variables; currencies in it are converted into the first one's terms, so money stays money and a ratio of two amounts is a number. Decimals with a dot whatever the locale; a name spelled like a currency is ignored. At the root a query naming a variable answers inline without a digit. X in <name> divides by it and says how many fit; a name that is the prefix of several (salary for salary_hour, salary_day) answers per member. |
Clipboard History (clipboard-history)
What you copied, searchable, with images. Text, images and file lists are
recorded by a watcher that runs while pal runs; the search is SQLite
full-text search over the text, ordered pinned first, then newest. The
palette opens with the detail pane showing: the source app over the
entry's name, the kind and pinned as chips, its numbers large (characters
and lines, pixels and size, a file count), then the full text (fenced), the
image or the file list, and when it was copied. A row that is a single url
gets the site's favicon; the source app and the time are accessories, and
a pinned entry sits under Pinned. An entry
you have named (⌘⇧R) is titled by its name from then on, with the text's
preview as its subtitle, and a search finds it by the name as well as by
the text (the name is matched as a substring, the text by prefix words).
Actions:
| action | shortcut | what |
|---|---|---|
| Paste | Enter |
hides the panel and pastes the entry into the app that was in front |
| Copy | ⌘Enter |
puts the entry back on the clipboard |
| Copy text from image | ⌘⇧T |
on an image entry: the text in it read by OCR (the Vision framework on macOS, tesseract on Linux when installed) goes on the clipboard as a copy of its own, "Copied text" in the HUD; an image with no text is a toast |
| Edit… | ⌘E |
on a text entry: a form with the text in a textarea; the submit copies the edited text, so it is the newest entry and the original stays as recorded; a box pastes it into the app in front as well |
| Pin / Unpin | ⌘P |
pinned entries sort first and never expire |
| Name… / Rename… | ⌘⇧R |
a one-field form: the name titles the row and is searched like the text; empty clears it |
| Save as file… | ⌘S |
a form with a folder (the Desktop by default, ~ expanded, created when missing) and a name taken from the entry (the text's first words .txt, Image 640x480.png, paths.txt); the text as it is, an image's PNG copied, a file list as its paths one per line; a name already there is refused with the form again |
| Save as snippet | ⌘⇧S |
on a text entry: the Snippets palette's create form, pre-filled with the text |
| Show as QR code | ⌘⇧K |
on a text entry up to 2000 characters: the code large in a level with the text under it |
| Delete | ⌘D |
removes the entry; asks first |
| Clear history | ⌘⇧D |
removes every entry, pinned ones included; asks first |
primary_action = "copy" swaps the first two, so Enter copies and
⌘Enter pastes. Marked rows (⇧↓, ⌘-click): Copy puts their texts
on the clipboard joined one per line (a file list its paths, an image its
title) as one copy, Delete removes each.
Paste needs the Accessibility permission on macOS: the paste is a
synthesised Cmd+V, which the system only delivers from a process on the
Accessibility list. Without it pal shows the system prompt once per run
and a toast, "Paste needs Accessibility. Grant pal in System Settings >
Privacy & Security > Accessibility", instead of half-doing it. On Linux the
paste is Ctrl+V through wtype, else ydotool (which needs ydotoold
running); with neither, paste fails and the toast says so.
What is never recorded: anything a password manager marks as concealed or
transient (the org.nspasteboard convention on macOS, the
x-kde-passwordManagerHint type on Linux, read through wl-paste or
xclip), pal's own concealed copies (a 1Password password, a
verification code: marked the same way, and their clear-after restore),
copies over 10 MB, and copies made while an app in the feature's
exclude_apps is in front. Copying something already in history bumps it to the top instead
of adding a duplicate.
Retention runs after every copy: unpinned entries older than
max_age_days are deleted, then the unpinned tail past max_entries.
Pinned entries never expire. A search lists at most 200 rows of what is
left, newest first after the pinned ones; type more to narrow it.
Link: pal://clipboard/copy?index=0 puts a history entry back on the
clipboard, 0 (the default) the newest (Links).
Pinned, filters and more actions
- Pinned entries sit in a Pinned section at the top; the rest follow without a header.
- A filter dropdown (
Tab) by kind: All, Text, Images, Files (the core's kinds), Links (text that is one url) and Colors (text that is one#hexorrgb()/rgba()colour). - A colour entry's icon is the colour itself (a tinted dot); an image row shows its size next to its dimensions.
- More actions: Open link (
⌘O) on a url, Paste as plain text (⌘⇧V) on text (the entry's text pasted as text), Copy image file (⌘⇧C) on an image (the PNG the core keeps, as a file), Delete all unpinned (asks first; every unpinned entry deleted one by one, since the core's only bulk operation is Clear). - The detail pane's header carries the entry's name when it has one.
Names live in the history database next to the entry (
namecolumn, added on the first open of an older database), so they survive a restart and go with the entry when it is deleted.
Settings, [extensions.clipboard]:
| key | type | default | what |
|---|---|---|---|
primary_action |
paste, copy |
"paste" |
What Enter does on an entry. |
ocr_concealed |
bool | false |
Text read from an image (Copy text from image) is copied concealed, so it never enters this history. |
What is recorded and for how long (exclude_apps, max_entries,
max_age_days) is the Clipboard history feature's, [features.clipboard]
(Features).
Clipboard (clipboard-rows)
What is on the clipboard right now, read as the things it could be: the Clipboard section of the empty root (before you type), and the same rows as a palette of their own. The rows are built from the history entry for the current clipboard, so what history never recorded (a concealed copy from a password manager, one made while an excluded app was in front, one over the size cap) never shows here either, and nothing leaves the machine except the page-title fetch below.
Each row's Enter is the one thing it is for; ⌘K has the rest, and every
row ends in Hide from the root (⌘⇧H), which keeps the section away
until the next copy:
| what is on the clipboard | rows | Enter | also |
|---|---|---|---|
| a web address | the page (its title fetched, 1 s at most, else the address; the site's favicon) and a QR code of it | Open | Open in private window (⌘⇧O), Copy as Markdown link (⌘⇧M), Copy URL, Shorten and copy (with shortener); the QR row: Show it large, Save to Desktop (SVG) |
a colour (hex, rgb(), hsl(), …, a CSS name) |
a swatch with the hex, rgb and hsl forms | Open in Colour Picker | Copy hex, rgb(), hsl() |
| a path, or lines of paths, or files copied in Finder | one row per path (three at most) with its size, then "N files" | Open | Reveal in Finder (⌘Enter), Open with… (⌘O), Copy name, Copy path; the count row: Reveal all, Copy paths, Copy names |
| an email address | the address | Compose | Copy address |
| a phone number | the number, with the digits | Call (tel:) |
Copy digits, FaceTime |
| JSON | JSON · 3 keys with its size and line count; the pretty form in the detail pane |
Pretty-print to clipboard | Minify to clipboard (⌘⇧M) |
an expression (2+2*3, (1+2)/4, 2^10, 15% * 80) |
2+2*3 = 8 |
Copy answer | Paste answer (⌘Enter), Copy expression = answer |
| a number | the number formatted, its hex, binary and octal | Copy without formatting | Copy as hex, Copy as binary |
| a unix timestamp (10 or 13 digits) or an ISO date | the moment in local time, how far off it is, the ISO form | Copy local time | Copy ISO 8601 (UTC), Copy unix seconds |
| hex or base64 that decodes to text | the decoded text | Copy decoded text | Paste decoded text (⌘Enter) |
| a tracking number (UPS, USPS, FedEx, DHL) | Track with the carrier | Open the carrier's page | |
a git sha, owner/repo#12, owner/repo |
the commit, the issue, the repo | Copy short sha / Open on GitHub | Copy full sha, Copy GitHub URL |
| an image | Image 640 × 480 with its thumbnail and size; Recognise text when the core can OCR |
Save to Desktop | Copy as PNG file (⌘⇧C), Paste; the OCR row: Recognise and copy, Recognise and paste |
| any text | the text with its word, character and line counts | Paste as plain text | Save as snippet (⌘S, the Snippets form pre-filled), Copy as Title Case, lowercase, UPPERCASE, as slug, trimmed |
A text is read as everything it is at once: 1700000000 is a number and
a timestamp, a path is a path and a text. Text over 64 KB keeps its
counts only. Every row has a detail pane (⌘I): the QR code large, the
swatch wide, the JSON pretty-printed, the file list, the image itself.
Settings, [palettes.clipboard-rows.settings]:
| key | type | default | what |
|---|---|---|---|
fetch_titles |
bool | true |
Ask a copied web address for its <title> (one request, 1 s at most, http and https only). The only thing these rows send off the machine. |
shortener |
text | empty | A URL with {url} in it whose response body is the short link, for the Shorten action (https://is.gd/create.php?format=simple&url={url}). Empty: no Shorten action. |
private_browser |
text | empty | Which browser opens a private window: Google Chrome, Brave Browser, Microsoft Edge, Chromium, Vivaldi or Firefox. Empty: the first of those that is installed. Safari has no such switch. |
Emoji (emoji)
A grid of every emoji in the bundled list, searched by name and keyword.
The tile is the glyph; the name is the shortcode with spaces. A catalog
at the root: 1906 rows, so a typed query shows at most three of them there
and the rest behind the "more" row, ranked under the primary and normal
rows that have the word.
Actions:
| action | shortcut | what |
|---|---|---|
| Copy emoji | Enter |
copies the glyph |
| Copy shortcode | ⌘⇧C |
copies :shortcode: |
Settings, per palette, [palettes.emoji.settings]:
| key | type | default | what |
|---|---|---|---|
columns |
number, 4 to 16 | 10 |
Tiles per row in the grid. Read once when the extension loads: after a change, Settings > Restart extension host. |
Sections, skin tone and shortcodes
- Sections: Recently used first (the last 24 you copied or pasted,
kept in the extension's storage), then Unicode's groups in order
(Smileys & Emotion, People & Body, ..., Flags).
data.jsonnow carriescategoryandskinper emoji (from unicode-emoji-json; the names and keywords are emojilib's as before). - The palette is
livewith a 30 sttl: the order is the sections' own, never frecency's, and a show more than 30 s after the last listing lists again so the recents follow what you used. - Search by shortcode:
:thumbs_up:is a keyword next tothumbs_up. - Actions: Copy emoji (
Enter), Paste emoji (⌘Enter), Copy shortcode (⌘⇧C);paste_by_defaultswaps the first two. - Skin tone:
skin_tone(none by default) is applied to the emoji that take one (329 of them: hands, people) in the tile, on copy and on paste. The modifier goes after the first code point, which tones a single person and the first person of a family or profession sequence; a two-person sequence gets one tone, on its first person. - Your words: the
keywordssetting adds search words per emoji (rocket: ship deploy), so a team's own names find them.
Settings, [extensions.emoji]:
| key | type | default | what |
|---|---|---|---|
skin_tone |
none, light, medium-light, medium, medium-dark, dark |
"none" |
The Fitzpatrick modifier applied where an emoji takes one. |
paste_by_default |
bool | false |
Enter pastes into the app in front (needs Accessibility on macOS), ⌘Enter copies. |
keywords |
list of lines | [] |
Your own search words: rocket: ship deploy, 🎉: party, woo (a shortcode or the emoji, a colon, the words, split on spaces and commas). A line naming no emoji is ignored; the words join the emoji's own for search, parsed once per settings change. |
Processes (processes)
What is running, from ps, listed again on every keystroke because the
set changes constantly (an input palette; the root only has its own row).
The query matches the name or a pid prefix. The row is the executable's
name, its pid the subtitle with the full path after it on macOS (outside
an app); the resident memory sits on the right, and a process
above 10% CPU carries a NN% cpu tag (orange, red from 50%). Rows are
sorted by CPU, then memory. On macOS a process that lives in a .app
bundle gets that app's icon.
The filter dropdown scopes the list:
| filter | rows |
|---|---|
| All | everything (the default) |
| Mine | your own user's processes |
| Top CPU | the 25 busiest |
| Top memory | the 25 largest, by resident memory |
Actions:
| action | shortcut | what |
|---|---|---|
| Kill | Enter |
SIGTERM, after a confirm; the palette stays open and lists again |
| Force kill | ⌘⇧K |
SIGKILL, after a confirm |
| Copy PID | ⌘C |
copies the pid |
A kill that fails (a process of another user, a pid that is gone) keeps the panel open with a toast carrying the OS's message.
Ports and Activity Monitor
- Kill by port: a query of
:and digits lists what listens on TCP ports instead of processes::3000that port,:30every port starting with 30,:alone every listener. One row per process and port with the port as a blue tag and the address as subtitle; a processpsknows gets its usual numbers and icon.ss -ltnpon Linux, elselsof -iTCP -sTCP:LISTEN(macOS ships it). Rows have the same kill and copy actions; a row's id ispid:port. - Open in Activity Monitor (
⌘O, macOS): brings Activity Monitor up and types the pid into its search field (⌘F, then the digits, through System Events; needs Accessibility, else the app just comes up).
Settings, [extensions.processes]:
| key | type | default | what |
|---|---|---|---|
include_system |
bool | false |
List system processes too: pids below 100, and kernel threads (children of kthreadd) on Linux. |
Quicklinks (quicklinks)
Your own links, kept in the extension's storage and edited in the panel.
At the root a typed web address (docs.rs/serde, https://…) gets one
inline Open row under a Quicklinks section (https:// is assumed when
there is no scheme), and a query nothing matched gets every link with a
{query} in it filled with the query as a fallback row.
Enter on a link opens it. A link whose url has a {query} placeholder
(https://github.com/search?q={query}; Raycast's {argument} and
{argument name="Repo"} are read the same way) drills in instead: the
input fills the placeholder as you type, percent-encoded, and Enter opens
the filled url (⌘C copies it). {selection} and {clipboard} in a url
are filled without asking (the SDK's placeholders, Snippets
below has the grammar; each value percent-encoded), so
https://translate.google.com/?text={selection} opens in one Enter. The
row shows the placeholder as a tag, the browser it opens with when one
is named, and the url (without https://) as its subtitle; the icon is the site's favicon.
The root row Create Quicklink opens a form (name, url, keywords, and
Open with: the default browser or one of the browsers installed:
Safari, Chrome, Firefox, Arc, Brave, Edge, Chromium, Vivaldi, Zen; open -a on macOS, the command on Linux; a mailto: or an app's scheme goes to
the system opener whatever the field says). The url and the name come
filled from the tab in front when Browser Tabs
can name one within 400 ms (a DevTools port, a scriptable browser); an
extension can push the palette with args: { create: { name, url, keywords } } and the form comes filled with that. Edit (⌘E) opens
the same form filled in, and a url the opener could not take (no scheme,
not a path) is refused with the message under the field. Delete (⌃X)
asks first. Keywords are extra words the search matches, space or comma
separated.
Browse Library (the row after Create) is a level of 25 ready-made
searches (Google, DuckDuckGo, Bing, Wikipedia, YouTube, GitHub, GitHub
code, npm, crates.io, PyPI, MDN, Stack Overflow, Amazon, Google Maps,
Google Translate, X, Reddit, Hacker News, IMDb, Spotify, Unsplash, Can I
use, Rust docs, Homebrew, Apple Developer): Enter adds one to your links
(once; the ones you have are tagged added), ⌘Enter searches with it
without adding (the same drill-in), ⌘C copies its url.
| action | shortcut | what |
|---|---|---|
| Open | Enter |
opens the url (in the named browser, or a tab already on the page with prefer_existing_tab), or drills in to fill its {query} |
| Copy URL | ⌘C |
copies the url as stored |
| Edit | ⌘E |
the form, filled in |
| Delete | ⌃X |
removes it, after a confirm |
| Add to my quicklinks | Enter |
in the library: the search saved to your links |
| Search with it | ⌘Enter |
in the library: the drill-in, without saving |
Link: pal://quicklinks/open?name=<name> opens a quicklink by name or
keyword; a {query} link opens the panel to fill it, or &query=<text>
fills it from the link (Links).
Import and export
- Import Quicklinks / Export Quicklinks and Import Snippets /
Export Snippets are rows after the list: each opens a form with one
path field (
~expanded; export defaults to~/Downloads/pal-quicklinks.jsonand~/Downloads/pal-snippets.json). Export writes your own rows as a JSON array ({name, url, keywords?},{name, text, keyword?}; no ids), replacing the file. Import reads such a file (Raycast's{name, link}quicklink export is read too), skips what you already have (a quicklink by url, a snippet by name and text), and says how many came in. A path that cannot be read or written is refused with the message under the field.
Settings, [extensions.quicklinks]:
| key | type | default | what |
|---|---|---|---|
import |
path | (none) | A JSON array of {name, url, keywords?} listed alongside your own links, read-only (Open and Copy URL only), read on every listing. ~ is expanded. A file that cannot be read lists nothing and says so in the log. |
prefer_existing_tab |
boolean | false |
Opening a link first asks the browsers (as Browser Tabs does) for a tab already on that page (origin and path; query and fragment aside) and switches to it, the front one first; a new tab when none has it. |
The links themselves live in <data dir>/pal/storage/quicklinks.json
(see Storage in Extensions), shared by every config
profile.
Snippets (snippets)
Short texts by name and keyword, kept in the extension's storage and
edited in the panel. Enter pastes the text into the app that was in front
(needs Accessibility on macOS, like Clipboard History), ⌘C copies it
instead. The keyword is a row keyword, so typing sig finds the
signature; it shows as a tag, the first line of the text is the subtitle,
and the whole text is in the detail pane (⌘I).
Placeholders in the text are filled in when it is pasted or copied:
| placeholder | becomes |
|---|---|
{clipboard} |
the newest text on the clipboard |
{date} |
today, YYYY-MM-DD |
{time} |
now, HH:MM |
{datetime} |
both, with a space between |
{uuid} |
a fresh UUID, a different one per occurrence |
{selection} |
the text selected in the app in front (the clipboard when nothing is) |
{snippet name=sig} |
another snippet's text, by name or keyword, its own placeholders filled; one level deep (a {snippet} inside it stays as written) |
{cursor} |
where the caret lands after an expansion (below); dropped by a paste from the panel |
{date}, {time} and {datetime} take two attributes. format= writes
the moment with the tokens YYYY YY MM DD HH mm ss ddd
(Wed) MMM (Sep), anything else in the format as it is: {date format=DD.MM.YYYY}, {time format=HH:mm:ss}, quotes around a format
with spaces ({date format="ddd D MMM"}). offset= moves the moment
first by a signed count of days, weeks, hours or minutes: {date offset=+1d}, {date offset=-2w}, {time offset=+3h}, {datetime offset=-90m format=HH:mm}.
Anything else in braces is left as it is, so a snippet of code keeps its
braces. A snippet with placeholders carries a "dynamic" accessory. The
grammar is the SDK's (expand in @zcag/pal, Extensions),
the same one quicklinks fill a url with and Obsidian an appended line.
An expansion by keyword (below) fills the plain forms only: format=,
offset= and {snippet} are left as written there.
The root row Create snippet opens a form (name, keyword, text); Edit (⌘E) opens it filled in; a keyword with a space in it is refused (one word, so typing it finds the row whole). Delete (⌃X) asks first.
| action | shortcut | what |
|---|---|---|
| Paste | Enter |
hides, then pastes the filled text into the app in front |
| Copy | ⌘C |
copies the filled text |
| Edit | ⌘E |
the form, filled in |
| Delete | ⌃X |
removes it, after a confirm |
Link: pal://snippets/paste?name=sig pastes the snippet by name or
keyword with the placeholders filled; ©=1 copies it instead
(Links).
The snippets live in <data dir>/pal/storage/snippets.json, shared by
every config profile.
Expansion: the keyword typed in any app
On macOS a snippet's keyword typed in any other app can be replaced by its
text in place: that is the Text expansion feature's, which keeps the
snippets this palette edits (Features). A
snippet saved here expands on the next keystroke. Linux has no expansion
(no portable keyboard tap); the palette's Enter is the way to paste a
snippet there, and pal://snippets/paste?name=sig binds one to a
compositor key.
SSH Hosts (ssh)
Every Host in ~/.ssh/config that is a name rather than a pattern
(*, ? and ! entries are skipped, a Host a b line gives two rows),
in file order. Include lines are followed one level: ~, absolute and
globbed operands work, a relative one is under ~/.ssh. A Match block
ends the current host. user@HostName is the subtitle (both keywords, so
the real name finds the alias), Port a tag.
With include_known_hosts on, the names in ~/.ssh/known_hosts (next to
the config) come after, in a second section: [host]:port unwrapped,
comma lists split, hashed lines, bare IPs and hosts already in the config
skipped.
Actions:
| action | shortcut | what |
|---|---|---|
| Connect | Enter |
opens a terminal window running ssh <host> |
| Copy host | ⌘C |
copies the host name |
| Copy ssh command | ⌘⇧C |
copies ssh <host> |
Which terminal Connect opens:
- macOS: the
terminalsetting.autotakes the first installed of kitty, Ghostty, Alacritty, iTerm2, Terminal (Terminal is always there, so it is the fallback). kitty getskitty -1 ssh host(a new OS window in the running instance when that was started single-instance), Ghostty and Alacrittyopen -na <app> --args -e ssh host, Terminal and iTerm2 an AppleScript that opens a window running the command. A terminal that is picked but not installed keeps the panel open with a toast. - Linux:
$TERMINAL, else the first of kitty, foot, alacritty, xterm on PATH; kitty and foot take the command as arguments, the others after-e. Same rule as the Applications palette's terminal entries.
Sections, ProxyJump and Ping
- The section is the file a host came from, relative to the config's
directory (
.ssh/config,.ssh/conf.d/work.conf); known hosts stay a lastKnown hostssection. ProxyJumpis read: the row carries avia <jump>tag and the jump as a keyword, and Copy ssh -J command (⌘⇧J) copiesssh -J <jump> <host>(the plain command already goes through the config's ProxyJump; the-Jform is for a machine without it).- Ping (
⌘P): one echo to the HostName (else the name), the round trip as a toast (marko.lan: 3 ms), or why it did not answer.
Settings, [extensions.ssh]:
| key | type | default | what |
|---|---|---|---|
config |
path | ~/.ssh/config |
The config to read. ~ is expanded; known_hosts is looked for next to it. |
include_known_hosts |
bool | false |
List the names in known_hosts too, in a second section. |
terminal |
auto, kitty, Terminal, iTerm2, Ghostty, Alacritty |
auto |
macOS only: which terminal Connect opens. |
Files (files, files-browse, files-selection, files-recent)
An input palette over the operating system's own file index: what you type
is a name search on every keystroke, never a walk pal indexes itself. A
typed path (~/Down, /usr/local/bin) completes instead of searching:
the file itself when it exists, else the entries of its folder that start
with the last segment (hidden ones only when the segment starts with a
dot or show_hidden is on); at the root the same rows show inline under
a Files section. A path ending in / (~/, /usr/local/) lists that
folder whole, as browsing does. A root query nothing
matched gets a "Search Files for
“…”" fallback row that opens the palette with it typed. The
row is the file name; the parent folder is the subtitle (home shortened to
~); the size and the modification date are accessories. .app bundles
get their own icon, everything else a glyph by kind (folder, image,
document, code, archive). Exact and prefix name matches come first, the
rest in the backend's order, at most limit rows. With nothing typed the
palette shows one hint row naming the backend and the folders it searches.
The backend is picked once when the extension loads:
| platform | backend | how |
|---|---|---|
| macOS | Spotlight | mdfind -name <query> -onlyin <folder>...; case-insensitive substring of the display name, so kitty finds kitty.app |
| Linux | fd |
fd --absolute-path --fixed-strings --max-results <limit> --max-depth 8 --exclude <x>... <query> <folders>; respects .gitignore like fd does everywhere |
| Linux, no fd | locate |
locate -i <query>, filtered to the folders (updatedb decides how fresh it is) |
| Linux, neither | find |
find <folders> -iname '*<query>*'; walks the folders on every keystroke, and a second hint row says so |
Every search is one process: killed after 3 s, killed as soon as limit
paths have been read, and killed when the next keystroke starts a new one.
The exclude folders are pruned from the walk (fd, find) or dropped from
the answer (Spotlight, locate); fd also stops eight levels down, since with
fewer than limit matches it would otherwise walk the whole home. The
backend picked is logged once at load ([files] backend: fd).
Contents are searched too. A plain query lists the name matches, then
a second section, "In files", of files whose text contains the query,
each row's subtitle the first matching line (12: the line collapsed to
one line, then the folder). A query starting with ' or content:
('invoice total, Alfred's in prefix) searches contents only. The
content backend, picked once at load and logged ([files] content: mdfind): Spotlight's text index on macOS (mdfind -onlyin <folder>... 'kMDItemTextContent == "*<query>*"cd', so PDFs and documents count, and
only what Spotlight has indexed), rg --files-with-matches on Linux when
ripgrep is installed, else grep -rlI. One process per keystroke, killed
after 1 s: what it printed by then is the answer. The snippet is rg -n -m1 (or grep -n -m 1) per file, eight at a time; a file the tool
cannot read as text (a PDF Spotlight matched) keeps the folder as its
subtitle. A file both searches find is listed once, as the name match.
The name matches show as soon as they are ranked; the "In files" rows
join below them as each batch of snippets comes in.
content_search = false keeps the second section off; the prefix still
works.
The detail pane (lazy, asked when the cursor rests on a row) leads with
the folder over the name, the kind and Finder's tags as chips, and the
size and an image's pixels large, then when it changed. The tags and
pixels are what Spotlight knows of the file on macOS (one mdls call,
only when the pane asks; the colour index is dropped). On Linux a PNG's
size comes off its header. For a text file under 64 KB the
first 40 lines follow in a code block. No image preview: the app's
icon:// scheme serves app icons, favicons and clipboard images only.
Actions:
| action | shortcut | what |
|---|---|---|
| Open | Enter |
the system opener; on a folder row this is Browse (below), Open second |
| Browse | Enter, → |
a folder's contents as a level (below); → from anywhere in a listing while nothing is typed |
| Reveal in Finder / Show in file manager | ⌘Enter |
open -R on macOS, xdg-open on the parent folder on Linux |
| Open with… | ⌘O |
a level listing the apps registered for the file (below) |
| Copy path | ⌘C |
copies the absolute path |
| Copy file | ⌘⇧C |
the file itself onto the clipboard: a paste in Finder or a file manager copies it, a paste in a text field gets its path |
| Copy text (OCR) | ⌘⇧T |
on an image or a PDF: the text in it (the PDF's first page) read by OCR onto the clipboard, "Copied text" in the HUD; the Vision framework on macOS, tesseract on Linux when installed (a toast otherwise). A PDF page is rendered by pdftoppm when installed, else sips on macOS |
| Open in Terminal | ⌘T |
a terminal window in the folder (a file's folder): the app the terminal setting names, through the Shell extension's table (Terminal and iTerm over AppleScript; kitty, Alacritty, WezTerm, Ghostty by their flags; Linux $TERMINAL or the first installed) |
| Rename… | ⌘⇧R |
a form with the name; the same folder, a slash or a taken name refused with the message under the field |
| Move to… | ⌘M |
a form with the folder (~ expanded, made when missing); across volumes mv does it |
| Copy to… | ⌘⌥C |
the same form; a copy under the same name (a folder whole), an existing name refused |
| Compress | ⌘⇧Z |
a zip next to the file named after it (report.zip, report-2.zip when taken); with rows marked, one zip of them all named after the first. ditto -c -k --sequesterRsrc --keepParent on macOS, zip -r on Linux |
| Move to Trash | ⌘D |
asks first; Finder's delete on macOS, gio trash on Linux; the palette stays open with a toast |
| Use in TextEdit's open panel | ⌘G |
only while the app in front has an Open or Save panel up: pal hides and types the path into it through its Go to Folder sheet (ctrl+L on a GTK chooser); listed first then, and the empty root leads with a "Dialog" hint into Files |
Marked rows (Tab here, x while nothing is typed, ⇧↓, ⌘-click): Open,
Reveal, Quick Look (one panel, arrows between them), Copy path (the paths
one per line), Copy file, Compress (one archive) and Move to Trash run
over all of them as one pick; Open with…, the terminal and the three
forms stay one file's.
Browsing folders
Enter (or →) on any folder row, in a search, the recents, a completion
or the root's inline Files section, pushes the Browse Folder palette
(browse) with that folder: a level whose crumb is the folder's path
(~/proj), led by a .. row and followed by the entries under the
dropdown's sort. Enter, ← or Backspace on the .. row goes up, and
← or Backspace from any row does too while nothing is typed
(LaunchBar's and Alfred's convention); → on a folder goes in. There is
no .. row at /. Typing filters the listing by name (a case-insensitive
substring); Tab cycles the sort:
| sort | order |
|---|---|
| Name | folders first, then files, each case-insensitive alphabetical (Finder's) |
| Date | newest first, folders and files mixed |
| Size | largest first, folders (no size of their own) last |
Hidden entries (a leading dot) show only with show_hidden, which ⌘.
flips from any row (it writes the setting, so every listing follows; the
action's title says which way). A picture draws its own thumbnail in
place of the kind glyph (icon://localhost/file, the app's scheme). A
folder over 500 entries shows that many and a last, inert row counting
the rest; typing narrows past the cap. File rows carry the same actions
as search rows (⌘I for the pane; Tab, x, ⇧↓ mark rows as in
Files), the .. row's pane describes the folder it leads to, and Open
with… from a browsed row pushes the apps level on this palette. Inside
Files, a typed path ending in / that names a folder lists it the same
way (the .. row, every entry sorted by name, the cap), so ~/ is a
way in without a search; a path without the slash still completes the
last segment. Opened by name from the root (its own row, "Browse
Folder") it lists the home folder.
Open with… drills into a level of the applications the OS registers
for the file, each with its own icon: the default (what Enter would use)
first with a default tag, the rest by name; typing narrows them by name
or bundle id, and Enter opens the file with that app (open -a on
macOS, gio launch on Linux) and hides the panel. On macOS the list is
Launch Services' (NSWorkspace, every role); on Linux the file's MIME
type (xdg-mime query filetype, else file) is looked up in the
mimeapps.list files and every data dir's mimeinfo.cache, the default
from xdg-mime query default, and a text/* file also gets the
text/plain editors. A file nothing is registered for shows one hint
row.
Copy file writes file URLs on macOS and text/uri-list on Linux
(X11, or Wayland with the wlr data-control protocol; a compositor without
it gets a toast saying so). The clipboard history records it as a files
entry like any copy.
The Finder selection
Open pal over a Finder window (or the Desktop) with files marked and the
empty root leads with a Selected in Finder section, nothing typed:
each marked item as a file row with every action above, pictures with
their thumbnails; with two or more, an N items row first (the names,
the total size) whose actions run on all of them at once: Open all,
Reveal all, Quick Look all, Copy paths, Copy files, Compress together,
Move all to Trash (asks first), and whose Enter opens the palette with
the whole selection. The root shows four item rows at most.
Finder Selection (files-selection) is the same as a palette of its
own, for a hotkey or pal open files/selection: the N items row, then
every marked item, typing filters them by name, marks and the multi
actions work as anywhere. With nothing to list it says why: "Nothing is
selected in Finder" with Finder in front, "Finder is not in front"
otherwise (the front_app state), "Not available on Linux" there, since
no file manager exposes its selection.
The selection is the core's selection.files() (Finder's selection
over osascript, only while Finder is the app in front; ~190 ms, read
once per panel show and cached), so it is what was marked when the panel
came up: a folder shown with nothing marked is nothing, not the folder,
and a selected dot file is listed whatever show_hidden says. The
Images palette reads the same selection for its inputs, and {files} in
a snippet or a quicklink fills in the paths.
Recent files and Quick Look
- Before you type, the Files palette lists the recently used files
(a
Recently usedsection) instead of the hints: on macOS Spotlight'skMDItemLastUsedDateover the last seven days within the configured folders (mdfind -attr, so the rows sort newest first), on Linux GTK's~/.local/share/recently-used.xbel. Folders, hidden and excluded paths and files that are gone are left out; the date on the right is when the file was last used. - Recent Files (
files-recent) lists the same rows as a palette of its own: live (newest first is the order), listed again on a show once the listing is a minute old, with the same actions and detail pane. - Quick Look (
⌘Y) on macOS opens the file inqlmanage -p(every marked row in one panel). The System palette's "Quick Look Finder Selection" row does the same for what is marked in Finder without listing it first.
Settings, [extensions.files]:
| key | type | default | what |
|---|---|---|---|
folders |
list of paths | ["~"] |
Where to search. ~ is expanded. |
limit |
number, 1 to 500 | 50 |
At most this many rows per query. |
show_hidden |
bool | false |
List files and folders whose name starts with a dot (below the configured folder; ~/.config as a folder is fine either way). |
exclude |
list of names | ["node_modules", ".cache", "Library/Caches", "target"] |
Folders skipped below the search folders, by name or a short path. |
content_search |
bool | true |
The "In files" section under the name matches. Off, only the ' prefix searches contents. |
ocr_concealed |
bool | false |
Text read from an image (Copy text) is copied concealed, so it never enters the clipboard history. |
terminal |
string | "" |
What Open in Terminal opens: Terminal (the default), iTerm, kitty, Alacritty, WezTerm, Ghostty, or any app name (open -na <name> --args -e ...); on Linux a command name, else $TERMINAL, else the first installed terminal (the same words as Shell's setting). |
The rename, move and copy forms and the archive command are the SDK's
files (sdk/src/files.ts); Downloads uses them too, so the two palettes
rename and move the same way.
System (system)
Sleep, lock, log out, restart, shut down, empty the trash, dark mode,
volume, brightness, do not disturb, eject, show desktop, keep awake (for a
while, until a time, or while an app runs, with a countdown on the bar),
quit or unhide every app, dismiss notifications. The rows are indexed, so
mute or sleep at the root finds them (each carries keywords:
suspend, power off, bin); live because the Keep Awake row reads
Allow Sleep with the time left while a run is on, and a live palette lists
again on every show. pal hides the panel before running a command, so it
lands on the desktop, not on pal.
Only commands this machine can run are listed:
| command | macOS | Linux |
|---|---|---|
| Sleep | pmset sleepnow |
systemctl suspend |
| Sleep Displays | pmset displaysleepnow |
Hyprland (hyprctl dispatch dpms off) or Sway (swaymsg output * dpms off) only |
| Lock Screen | the login framework's immediate lock, falling back to the Cmd+Ctrl+Q keystroke (which needs Accessibility) | loginctl lock-session |
| Log Out | System Events | hyprctl dispatch exit, swaymsg exit, else loginctl terminate-session |
| Restart, Shut Down | System Events | systemctl reboot, systemctl poweroff |
| Empty Trash | Finder | gio trash --empty |
| Toggle Dark Mode | System Events appearance | gsettings color-scheme between default and prefer-dark |
| Volume Up, Down, Toggle Mute | 10% steps, System Events | wpctl, else pactl, 10% steps |
| Brightness Up, Down | needs the brightness CLI on PATH; hidden otherwise |
brightnessctl, 10% steps |
| Toggle Do Not Disturb | runs a Shortcut named "Toggle Do Not Disturb"; hidden until you create one (Focus has no CLI) | swaync-client, makoctl or dunstctl; hidden with none |
| Eject All Disks | Finder | not available |
| Show Desktop | Mission Control | not available |
| Keep Awake / Allow Sleep | caffeinate -i (-di with the display), -t for the deadline, -w to follow an app; below |
systemd-inhibit --what=sleep (idle:sleep with the display) around sleep <secs>, tail --pid or timeout; a hint row without systemd-inhibit |
| Quit All Apps | System Events: every regular app but Finder and pal asked to quit, one by one, so an app with unsaved work still shows its sheet | not available |
| Unhide All Apps | System Events: every hidden app made visible | not available |
| Dismiss Notifications | Notification Center over Accessibility: the Clear All (else Close) action of every notification group; nothing on screen is nothing to do | swaync-client --close-all, makoctl dismiss --all or dunstctl close-all; hidden with none |
| Quick Look Finder Selection | the Quick Look panel (qlmanage -p) over what is marked in Finder (the core's selection.files(), read once per show): the names as the subtitle, the count on the right; while nothing is marked, or Finder is not in front, the row is inert and its subtitle says which |
not available |
Log Out, Restart, Shut Down, Empty Trash and Quit All Apps are destructive: with
confirm_destructive on, Enter asks "(command) now?" first. A command
that fails keeps the panel open with a toast carrying the tool's message.
Link: pal://system/run?id=<command> runs one by its id (sleep,
lock, logout, restart, shutdown, empty-trash, dark-mode,
volume-up, volume-down, volume-mute, brightness-up,
brightness-down, dnd, eject-all, show-desktop, keep-awake (the
toggle), quit-all, unhide-all, dismiss-notifications, and on macOS
quick-look-selection, the one for a Quick Look hotkey: the HUD says
"nothing is selected in Finder" when there is nothing to show); the route
is declared with confirm, so a link always asks first
(Links). pal://system/awake?for=1h&display=1
is Keep Awake's own (below).
Keep Awake
The row takes how long in the bar (Item.args): 45m, 2h, 1h30m, a
bare number of minutes, a clock time (14:30, 2pm, until 14:30;
tomorrow's when today's has passed), or forever; blank runs
awake_default (an hour). A second field says whether the display stays
up too (awake_display preselected). Enter starts it, the panel hides
and the HUD says what it did (Awake for 45 min, Awake until 14:30,
Awake until turned off); cmd+enter keeps awake until turned off;
cmd+u opens a form with the same spelling, the display switch and an
app to follow (one with a window open: the run ends when it quits,
caffeinate -w). While a run is on the row reads Allow Sleep with the
time left as a tag: Enter ends it (Sleep allowed), Keep awake for…
takes the bar's fields for a new span, cmd+d flips the display; the
row sits in the empty root's Now section meanwhile. A spelling that is
neither a duration nor a time is a failure toast (under the field, in
the form).
caffeinate does the timing itself (-t <secs>), so a run ends on time
whether or not pal is up. pal keeps one record of the run (pid, until,
display, app) in its storage and checks it against the live process on
every read (the pid alive, its command line caffeinate's, the deadline
not passed); the child is not killed with the host, so a restarted pal
finds the same run back, and a record whose process is gone (or is
something else after a reboot) is dropped silently. A run that ends while
pal watches says so in the HUD (Keep awake ended, sleep allowed).
Bar item system/awake: a coffee glyph and the countdown (2h 40m,
12m, 45s; ∞ without an end), a monitor mark after it when the
display is kept awake too, amber in the last five minutes (the ending
rule, over system.awake_left); hidden while off, and
[bar.items."system/awake"] show = "always" keeps a muted coffee whose
click still opens the popover. The popover: the run on a card (what it is,
since when, the time left large, a bar), the presets (the item's presets setting) as
tiles on the digits 1..5 (a new end from now, on or off), the display
switch on d (a run is restarted with the other flag; off, it is the next
run's), u a field for a spelling, Enter allows sleep while on and
starts the default while off, backspace allows sleep, o the System
palette. The ticks are pal's own: a push when the countdown's text next
changes (every minute; every second under one, or while the popover is
up). States published: system/awake, system/awake_until (unix ms,
null without an end), system/awake_left (minutes, rounded up),
system/awake_display.
Link: pal://system/awake?for=1h&display=1 (until=14:30,
app=Xcode, off=1; bare toggles: the default duration, or off while
on); pal call system/awake for=2h from a shell.
Trash count and dark mode
- Empty Trash shows what is in the Trash on the right (
3 items,empty);~/.Trashitself is readable only with Full Disk Access, and without it the row simply has no count. Linux reads~/.local/share/Trash/files. - Toggle Dark Mode carries the current appearance as a tag (
darkviolet,lightamber), fromdefaults read -g AppleInterfaceStyleon macOS and GNOME'scolor-schemeon Linux.
Settings, [extensions.system]:
| key | type | default | what |
|---|---|---|---|
confirm_destructive |
bool | true |
Confirm before logging out, restarting, shutting down, emptying the trash or quitting every app. |
awake_default |
string | "1h" |
What a bare Keep Awake runs for: a duration, a clock time, or forever. |
awake_display |
bool | true |
Keep the display awake too (caffeinate -d); off, the display may sleep while the machine stays up. The row's field and the popover's switch override it per run. |
awake item settings, [bar.items."system/awake".settings]:
| key | type | default | what |
|---|---|---|---|
presets |
list of strings | ["30m", "1h", "2h", "forever"] |
The popover's tiles and digit keys, five at most. |
Windows (windows, windows-spaces)
Every open window with its app's icon, most recently used first on macOS
and Hyprland (front to back on Sway and X11), never ranked by how often
it was picked: row 1 is the window you came from, row 2 the one before
it. Hyprland keeps that history; on macOS pal does, stamped on every app
activation, when the panel shows, and by Focus, so a focus change inside
one app shows up on the next activation. A live palette: the list runs on
every show, so window titles are root results. The app name is the
subtitle; the bundle id or window class is a keyword; a hidden app's
window carries a hidden tag, a minimised one minimized, one on another
workspace or space space <n> or other space, and the monitor when known.
Actions:
| action | shortcut | what |
|---|---|---|
| Focus | Enter |
hides the panel, then raises the window, restoring it if minimised |
| Close | ⌘W |
closes the window; the palette stays open and lists again |
| Minimize | ⌘M |
minimises; not offered on a window that already is |
Marked rows (Tab here, x while nothing is typed, ⇧↓, ⌘-click): Close and
Minimize run over all of them; Focus is one window.
- macOS: the list comes from CoreGraphics merged with the Accessibility API for the parts CoreGraphics does not give (another app's window title, whether it is minimised). Without the Accessibility permission the list still works (titles only when Screen Recording allows, else the app name), focusing falls back to activating the app, and close and minimise fail with "needs Accessibility permission". Focus shows the same one-time prompt and toast as paste when the permission is missing.
- Linux: the backend is detected, not configured. Hyprland when
hyprctland an instance signature exist (the newest$XDG_RUNTIME_DIR/hypr/*when the variable is missing, so a service-started pal works); Sway onSWAYSOCK; X11 onDISPLAYpluswmctrl. Hyprland has no minimise, so pal parks the window on thespecial:minimizedworkspace and Focus brings it back. Sway minimise ismove scratchpad; X11 minimise needsxdotool. With none of the three the palette reports "no Hyprland, Sway or X11 (wmctrl) session".
Grouping and app actions
- Rows are grouped by app (a section per app, in the order the front window of each gives); the app name is a keyword as well as the subtitle.
- More actions: Hide app (
⌘H, macOS: System Events hides the window's process), Show app in its place on a hidden app's window (⌘⇧H, macOS: the app comes forward with every window it had, and the panel hides), and on an app with more than one window Minimize all of this app (⌘⇧M) and Close all of this app (⌘⇧W, asks first).
Spaces
Spaces (windows-spaces) is the same capability one level up: a row
per Space (macOS), workspace (Hyprland, Sway) or desktop (X11) in the
order the desktop shows them, the apps on it as the subtitle (most
recently used first, each once), the icon of the window used there last
(a monitor glyph when nothing is open there), a current tag on the one
in front and previous on the one left most recently, and the display
when there is more than one. A full-screen app's space (macOS) is a row
named by the app. The last row, Previous space, goes back to the one
left most recently: a switch by pal or a swipe, pal watches the Space
change either way.
Enter brings the space in front: the panel hides first. macOS has no
public call to switch Spaces, so pal does what yabai does with SIP on: a
synthesised Dock swipe at a velocity no finger reaches, one step per
space between along that display's strip, which moves the strip without
the slide (instant), and then raises the window used there last (the
focus history, else the biggest, never a hidden app's) so the space comes
up with it in front. Posting the swipe needs the Accessibility permission
like paste (the HUD says so when it is missing). Linux asks the
compositor (hyprctl dispatch workspace, swaymsg workspace,
wmctrl -s).
Names: spaces = ["web", "term", "misc"] names the desktops by number
(the first entry is Desktop 1). A named space's row is titled by it, the
Windows rows say space term instead of space 2, and its row id is the name,
which is what a global hotkey keys on:
[extensions.windows]
spaces = ["web", "term", "misc"]
back_and_forth = true
toggle = ["web", "term"]
[palettes.windows-spaces.item_hotkeys]
web = "ctrl+1"
term = "ctrl+2"
misc = "ctrl+3"
toggle = "ctrl+f"
An unnamed desktop's id is its number ("4"); last is Previous space.
With back_and_forth on (Hyprland's workspace_back_and_forth), a
space's key pressed while on that space goes to the previous one, so one
key toggles between a space and where you came from; Enter on the
current space's row does the same. toggle = ["web", "term"] adds a
Toggle row (toggle) that flips between those two: on the first it
goes to the second, anywhere else to the first, so toggle = "ctrl+f"
in the hotkeys block is a fixed ping-pong between two spaces however you
got where you are. pal run windows/spaces/term from a script or a
compositor keybind is the same pick.
Settings, [extensions.windows]:
| key | type | default | what |
|---|---|---|---|
include_minimized |
bool | true |
List minimised windows too (focusing one restores it). |
spaces |
list of strings | [] |
Names for the desktops by number: the row titles, the ws accessory, and the row ids item_hotkeys key on. |
back_and_forth |
bool | false |
A space's hotkey pressed while on it goes to the previous space. |
toggle |
list of two strings | [] |
Two space names (or numbers) the Toggle row flips between. |
The switcher (Keyboard): the manifest suggests
hold = "alt+tab" for this palette, so holding Alt and pressing Tab
shows the windows flat with the cursor on row 2, Tab again steps down,
Shift+Tab up, and letting go of Alt focuses the row under the cursor;
typing filters meanwhile, Escape cancels. [palettes.windows] hold = ""
turns it off, another chord moves it; on Linux a compositor keybind runs
pal switch instead.
Window Management (window-management, window-management-arrange)
Move and resize windows from the keyboard, Raycast's set: one row per
layout (halves, thirds, quarters, the maximize family, larger and smaller,
a size typed in, a nudge by a step, the other display, fullscreen,
minimize, restore),
Enter applies it to the window you were in. pal hides its panel
first, so the window with focus is the one behind the panel, not pal; the
HUD then names the layout, or says why it did not happen ("Restore: nothing
to restore", "Next Display: only one display").
| layout | id | what |
|---|---|---|
| Left Half, Right Half, Top Half, Bottom Half | left_half right_half top_half bottom_half |
half of the screen |
| Left Third, Center Third, Right Third | left_third center_third right_third |
a third |
| Left Two Thirds, Right Two Thirds | left_two_thirds right_two_thirds |
two thirds |
| Top Left, Top Right, Bottom Left, Bottom Right Quarter | top_left_quarter top_right_quarter bottom_left_quarter bottom_right_quarter |
a quarter |
| Maximize, Almost Maximize | maximize almost_maximize |
the whole screen; almost_maximize_percent of it, centred |
| Maximize Height, Maximize Width | maximize_height maximize_width |
the full height or width, the other side kept |
| Center, Reasonable Size | center reasonable_size |
centred as it is; reasonable_size_percent of the screen, centred |
| Larger, Smaller | larger smaller |
10% bigger or smaller about the centre |
| Resize to… | resize |
a form: a size (1280x720, or one number for a square) and, optionally, X and Y; blank keeps the window centred on its current centre, and the size is capped to its screen |
| Move Left, Right, Up, Down | move_left move_right move_up move_down |
nudged by step pixels |
| Next Display, Previous Display | next_display previous_display |
the same place on the other display |
| Toggle Fullscreen, Minimize, Unminimize | fullscreen minimize unminimize |
window state, not a frame (below) |
| Restore | restore |
the frame the window had before the run of layouts |
These ids are what item_hotkeys (below) and the
pal://window-management/layout?name=<id> route take
(Links).
What it does not do
No custom layouts: the thirty-one layouts above plus the Resize to… form
are the set; gap, step, cycle and the two percentages are the
knobs. Larger and Smaller are a fixed 10%. Resize to… is a form, not a
link: pal://window-management/layout takes the layout ids only.
"The screen" is the display the window's centre is on (the one it overlaps
most when the centre is off every display), minus the menu bar, Dock, or
bars, minus gap on every side; the halves, thirds and quarters are equal
cells with gap between them. Restore remembers, per window and in memory
until pal quits, the frame a window had before a run of layouts started: a
run is any sequence of pal layouts, and moving the window by hand in
between starts a new one, so Restore goes back to where you had put it.
With cycle on (a setting, off by default), a half applied to a window
already at that half steps to the next size of its family, Rectangle's
way: Left Half, then Left Two Thirds, then Left Third, then the half
again (the right family likewise; the top and bottom halves have none).
The HUD names the size the window landed on, and Restore still goes
back to where the run started.
Actions:
| action | shortcut | what |
|---|---|---|
| Apply | Enter |
the layout on the focused window |
| Apply to… | ⌘Enter |
pick a window from the open ones (the Arrange Window palette), then the layout goes on that one |
| Resize | Enter in the Resize to… form |
the size (and place) typed goes on the window; the panel hides and the HUD says "Resized to 1280x720" |
Arrange Window (window-management-arrange) is the same thing the
other way round: an input palette of the open windows (minimised ones left
out); Enter on one lists the layouts with that window's title as the
subtitle, and Enter on a layout applies it there.
Per-layout global hotkeys, which move the focused window without showing
pal at all, are item_hotkeys under the palette in the config file (see
Config); the moves and the resizes are the ones
worth a key, since they repeat:
[palettes.window-management.item_hotkeys]
left_half = "ctrl+alt+left"
right_half = "ctrl+alt+right"
maximize = "ctrl+alt+enter"
restore = "ctrl+alt+backspace"
larger = "ctrl+alt+="
smaller = "ctrl+alt+-"
move_left = "ctrl+alt+shift+left"
move_right = "ctrl+alt+shift+right"
move_up = "ctrl+alt+shift+up"
move_down = "ctrl+alt+shift+down"
fullscreen = "ctrl+alt+f"
minimize = "ctrl+alt+m"
Coming from Rectangle, its default keys as one block, with cycle on so
a half pressed again steps through the thirds as it does there:
[extensions.window-management]
cycle = true
[palettes.window-management.item_hotkeys]
left_half = "ctrl+alt+left"
right_half = "ctrl+alt+right"
top_half = "ctrl+alt+up"
bottom_half = "ctrl+alt+down"
top_left_quarter = "ctrl+alt+u"
top_right_quarter = "ctrl+alt+i"
bottom_left_quarter = "ctrl+alt+j"
bottom_right_quarter = "ctrl+alt+k"
left_third = "ctrl+alt+d"
center_third = "ctrl+alt+f"
right_third = "ctrl+alt+g"
left_two_thirds = "ctrl+alt+e"
right_two_thirds = "ctrl+alt+t"
maximize = "ctrl+alt+enter"
center = "ctrl+alt+c"
restore = "ctrl+alt+backspace"
larger = "ctrl+alt+="
smaller = "ctrl+alt+-"
next_display = "ctrl+alt+cmd+right"
previous_display = "ctrl+alt+cmd+left"
Toggle Fullscreen, Minimize and Unminimize are window state, not a frame: Restore does not undo them, and Unminimize picks its own window (the one Minimize last put away while it is still minimised, else the frontmost minimised one), so it has no Apply to… and is not offered for a picked window. Larger and Smaller scale each side by 10% about the centre and are then pushed back inside the screen where they fit; the moves stop at the screen's edge; both leave a window larger than the screen where it is.
- macOS: needs the Accessibility permission (the frame is set through
the window's
AXPositionandAXSize); without itEntershows the same toast as paste and asks once. The displays areNSScreen's frames withvisibleFramefor the usable part, so an auto-hidden menu bar or Dock gives the whole screen. Apps keep their minimum size and may round. - Linux: Hyprland (
movewindowpixel exact/resizewindowpixel exact; a tiled window is floated first, since an exact frame means nothing inside the tiling layout; the display ismonitors -jwithreservedtaken out; Toggle Fullscreen isfocuswindowthendispatch fullscreen 0, since that dispatcher takes no window), Sway (floating enable,move absolute position,resize set,fullscreen toggle; the workspace rect is the usable part), or X11 (wmctrl -i -r <id> -e,-b toggle,fullscreen;xrandr --listmonitorsfor the displays,wmctrl -d's work area for the usable part,xprop -root _NET_ACTIVE_WINDOWfor the focused window). Minimize is the Windows palette's (special:minimized, the scratchpad,xdotool), Unminimize its focus. Sway and X11 are written to the tools' documented shapes and unit-tested on fixtures, not run against a live session yet.
Settings, [extensions.window-management]:
| key | type | default | what |
|---|---|---|---|
gap |
number (px) | 0 |
Pixels between a window and the screen edge, and between two windows of a split. |
almost_maximize_percent |
number (%) | 90 |
How much of the screen Almost Maximize fills, centred. |
reasonable_size_percent |
number (%) | 60 |
How much of the screen Reasonable Size fills, centred. |
step |
number (px) | 32 |
How far Move Left, Right, Up and Down nudge the window. |
cycle |
bool | false |
A half applied to a window already at that half steps to the next size of its family (two thirds, a third, the half again). |
Store (store)
Every extension your registries list, in the panel: pal's own registry
first, then any added in Settings › Extensions › Registries. An input
palette: what you type narrows the list by name, title, tagline,
category, keywords and palette titles; the dropdown (Tab) filters to
Installed, Updates, Registries, or a shelf (Productivity,
Developer, System, Media, Reference, Fun, Integration). Every row is one
listed extension with its tile, its tagline as the subtitle, how it
stands (update, needs a newer pal, not updated, comes with pal,
installed, or why it cannot be installed here), the registry's name
when it is not pal's, and the category on the right. What has an update
leads the list under an Updates heading. Opened on All with nothing
typed, the rest follows as Featured (three worth meeting first that
are not installed: Spotify, GitHub, Solitaire, Calendar, Translate, Disk
Space, Hue, Typing, in that order) and then a section per category, by
title (the section names the category, so the row does not). A search or a filter is one list, the extensions whose
title the words start first. The detail pane (⌘I) leads with where it
comes from and its category over the title, how it stands as a chip, then
the description, what it does (the listing's features), the palettes and
the listing's screenshots, with the author, its status, its build (short
hash and date), whether it updates by itself, platforms, the pal install line and, for pal's registry, a link to the page.
| standing | Enter |
⌘Enter |
⌘C |
⌃X |
|---|---|---|---|---|
| not installed | Install (asks first) | Open store page | Copy pal install <name> |
|
| installed, update ready | Update | Open | Copy install command | Remove (asks first) |
| installed | Open (its first palette) | Open store page | Copy install command | Remove (asks first) |
| comes with pal | Open | Open store page | Copy install command |
Install, Update and Remove go through the core (the SDK's
extensions.install, update, remove) and wait until the extension
is loaded or gone; a toast says how it went and the list relists in
place. Whether something has an update is the core's one check; the
palette compares nothing. One that comes with pal is turned off in
Settings, never removed from here. The Registries filter lists each
registry and an Add registry row; Enter opens Settings › Extensions ›
Registries.
The first listing in ten minutes and ⌘R fetch every registry, the
cached rows shown meanwhile; a registry that did not answer says so above
the list, with how old its list is. No settings.
Scripts and data files (scripts)
The zero-code tier: every [palette.<name>] table of a scripts config file
becomes a palette, backed by a shell script speaking JSON lines or by a
json / jsonl / toml data file. Each such palette has the id
scripts-<name>. Its settings and the whole format are in
Scripts and data files. A data-file table of 100 rows or
more (the nerd and kde icon lists, chars) is a catalog at the root
unless the table sets tier itself.
Home Assistant (home-assistant-entities, home-assistant-services, home-assistant-areas)
Home Assistant over its REST API (/api/states, /api/services, one
/api/template render for areas), with a long-lived access token;
multi, so a second home is [instances."home-assistant@cabin"] with
its own url and token (Config).
Home Assistant (home-assistant-entities) is live: every entity of
the domains setting, listed again whenever the panel shows, so a light's
state at the root is the current one and kitchen light finds it from the
root. Favourites come first in their order, then the domains in the
setting's order, names sorted within. A row is the friendly name; the
subtitle is the domain and the area (when the entity has one); the entity
id, domain and area are keywords; the accessories are the state (a
coloured tag for on/off/open/locked/playing and the other known
states, the value with its unit otherwise) and when it last changed. The
icon is the domain's glyph; a light that is on is a dot in its colour
(rgb_color, else warm white). The filter (Tab) scopes it: All entities (the
domains setting), Every domain (the rest after those), then Lights,
Switches, Sensors, Binary sensors, Climate, Media players, People, Scripts,
Automations, Scenes; a domain filter shows its domain whether or not it is
in domains. The detail pane (lazy) shows the entity, its state
and its attributes.
Actions, by domain (Enter is the first; ⌘K has them all):
| domain | actions |
|---|---|
| light | Toggle, Turn on, Turn off, Brightness (⌘B: a level of presets 10/25/50/75/100 %, the current one tagged) |
| switch, fan, input_boolean, humidifier | Toggle, Turn on, Turn off |
| climate | Set temperature (a form: the temperature with the entity's range, the mode from its hvac_modes), Turn on, Turn off |
| cover | Open, Close, Stop (Close first while open) |
| lock | Lock, Unlock (both ask first; the one that changes the state comes first) |
| media_player | Play or Pause, Next track, Previous track, Volume (⌘U: presets), Turn off |
| scene | Activate |
| script | Run |
| automation | Trigger, Enable or Disable |
| button, input_button | Press |
| vacuum | Start, Return to dock |
| anything else (sensor, binary_sensor, person, ...) | Copy value |
Every row also has the domain's second action on ⌘Enter (turn on or
off, set, pause), Copy entity id (⌘C), Show attributes (⌘⇧A: a level
with the state and every attribute as rows, each copying its value, ⌘C
its name) and Open in Home Assistant (⌘O: the automation or script
editor, the history page for the rest). A service call keeps the panel
open and lists again, so the row shows the new state, with a toast naming
it; a call that fails is a failure toast with HA's answer. Entity ids are
the row ids, so [palettes.home-assistant-entities.item_hotkeys] with
"light.kitchen" = "ctrl+alt+k" toggles the kitchen light without showing
pal.
Home Assistant Services (home-assistant-services) is every service
from /api/services, by domain then name, the id (light.turn_on) as an
accessory and a keyword, HA's name and description when it gives them
(most core services carry none over REST; the key is title-cased instead).
Enter opens a form: the target as a select of the entities the service
takes (its domains, every entity for a service that targets any, no field
for one without a target), then the service's fields from its description:
a select selector is a select, a boolean one a checkbox, the rest text
with the example as placeholder and a number's range in the help line. A
group's fields are inlined; a collapsed group (HA's "advanced" section) is
left out. The submit calls the service with the values coerced by selector
(numbers, JSON for objects and anything typed as [...] or {...}, comma
lists for entity selectors), empty fields left out, and toasts what
changed; a number field that is not one comes back on the form. Copy
service id is ⌘C. Listed once an hour.
Home Assistant Areas (home-assistant-areas) is every area that has an
entity, with the count; Enter lists those entities. The mapping is one
template render (areas(), area_entities(), area_name()), kept ten
minutes for the entity rows' subtitles; an HA that refuses the template
API leaves the rows without areas and the palette with a hint row.
When something is wrong every palette is one inert hint row that says
what and where to fix it (Settings › Extensions › Home Assistant): no URL, a
URL without a scheme, no token, a keychain:/env: token that did not
resolve, a rejected token (401), a host that did not answer within the
timeout, or one that could not be reached. Every request carries the
timeout. A URL that redirects (an http:// one behind a proxy that
answers 301 to https://) is followed with the token kept, since fetch
would drop it on the new origin, and the log says which URL to set.
Settings, [extensions.home-assistant]:
| key | type | default | what |
|---|---|---|---|
url |
text | unset | Where Home Assistant answers, scheme included (http://homeassistant.local:8123). |
token |
secret | unset | A long-lived access token (your profile in HA, Security). A keychain: or env: reference in the file. |
domains |
list | light, switch, climate, media_player, cover, lock, fan, scene, script, automation, sensor, binary_sensor, person, input_boolean |
The domains listed at the root, in this order. |
favorites |
list | [] |
Entity ids pinned to the top, in this order, whatever their domain. |
timeout |
number (s) | 5 |
How long one request may take. |
Verification Codes (otp)
One-time codes out of your recent text messages, read straight from
Messages' database (~/Library/Messages/chat.db, read-only, one SQLite
query per listing). The last hours of incoming messages are scanned; a
message counts when it talks about a code (code, kod, şifre, OTP, PIN,
parola, passcode, verification, doğrulama, código, token and so on) and
carries a run of 4 to 8 digits that is not part of an amount, a date, a
time or a phone number; Google's G-123456 is taken by its digits. The
row is the sender as Messages knows it (a shortcode like AKBANK, an
alphanumeric originator, a number or an email; a number or an email that
is in Contacts shows the contact's name, from the AddressBook database
the same permission covers), the message text is the subtitle, the code
is a green tag on the right with how long ago it arrived. Sections are
Today and Earlier, newest first. A live palette: listed again on every
show, so the code that just arrived is at the top, and the codes are root
results while they are in the window. A message whose text is empty is
read from its attributedBody (macOS Ventura and later keep the text
there).
Actions:
| action | shortcut | what |
|---|---|---|
| Paste code | Enter |
hides, then pastes the code into the app in front (Accessibility, as for any paste) |
| Copy code | ⌘C |
copies the code |
| Copy sender | ⌘⇧C |
copies the sender as Messages stores it |
Permissions and platforms:
- Full Disk Access: Messages keeps the database behind it, and
without it SQLite answers "authorization denied" (
SQLITE_AUTH); the palette then has one row, "Full Disk Access needed", whose action opens Privacy & Security, Full Disk Access. Grant it to pal, open the palette again. There is no other way to read Messages. - A database SQLite reports locked (
SQLITE_BUSY) is copied aside with its-waland-shmand the copy is read; the copy is removed after. - A database that is not there ("No Messages database"), or any other read error, is one inert row with the reason.
- Linux: one "Unavailable" row; there is no Messages.
The bar item otp/latest-code puts the newest code on the strip, green,
for a minute after it arrived (hidden otherwise); a click copies it. The
same reader, the same permission.
Popover keys of otp/latest-code (rendered every 10 s; the arrows move the
cursor, a click sets it):
| keys | does |
|---|---|
enter |
Copy the code (concealed, clears after 30 s) |
p |
Paste the code into the app in front |
s |
Copy the sender |
o |
Open the Verification Codes palette |
Settings, [extensions.otp]:
| key | type | default | what |
|---|---|---|---|
hours |
number | 24 |
How far back to scan. |
senders |
list | [] |
Senders never listed, as Messages shows them (AKBANK, a number, an email); case does not matter. |
db |
path | ~/Library/Messages/chat.db |
The database to read. |
contacts |
path | `` | An AddressBook-v22.abcddb for names; empty reads every source under ~/Library/Application Support/AddressBook. |
1Password (onepassword-items)
Your 1Password items through the op CLI. One op item list --format json per listing, kept for ttl seconds so the vault filters and a
Refresh inside the ttl do not run the CLI (and its unlock prompt) again;
⌘R past the ttl asks it again. The row is the item's title, the vault
and the username the subtitle, a glyph by category (login and password,
secure note, card and bank account, identity, SSH key, API credential,
server and database, and so on), the primary website's host on the right,
favourites first with a favorite tag, then by title. The item's website
hosts, username, category, vault and tags are keywords, so github.com
finds the login. The detail pane leads with the vault over the title, the
category, favorite and tags as chips, then the username, the website as a
link and the update time. No secret is ever in a row,
the index or a log: a pick runs op item get for that one field and puts
the value on the clipboard.
Actions:
| action | shortcut | what |
|---|---|---|
| Copy password | Enter |
op item get <id> --fields label=password --reveal; "Copied password" in the HUD |
| Copy username | ⌘U |
the username field |
| Copy one-time code | ⌘T |
op item get <id> --otp, the current TOTP |
| Open in 1Password | ⌘O |
onepassword://view-item?i=<item>&v=<vault>&a=<account>, the app's own link (the account from op account list, when there is one or account names it) |
An item without the field (a secure note has no password, a login without TOTP has no one-time code) keeps the panel open with a toast carrying the CLI's message.
Signed in or not:
- Nothing signed in (or no account set up for the CLI at all) is one row,
"Sign in to 1Password", whose action opens the CLI's sign-in page. Sign
in with
eval $(op signin)in a terminal, or turn on the desktop app integration (1Password, Settings, Developer), then⌘R. - With the app integration on, every CLI call that needs the vault shows 1Password's own unlock prompt (Touch ID on a Mac); pal waits up to a minute for it.
opnot installed (looked up on PATH, then/opt/homebrew/bin,/usr/local/bin,/usr/bin) is one row whose action opens the install page.
Settings, [extensions.onepassword]:
| key | type | default | what |
|---|---|---|---|
account |
text | `` | Passed as --account to every call: a shorthand, sign-in address, account id or user id. Empty uses the CLI's default account. |
vaults |
list | [] |
Only these vaults are listed, and each is a filter in the palette (All vaults first). Empty lists every vault with no filter. The filters are read when the extension loads, so a change shows after the host restarts. |
ttl |
number (seconds) | 300 |
How long the item list is kept before op is asked again. |
Browser Tabs (browser-tabs-tabs)
Every open tab of your browsers, in the browsers' own order, never ranked. A live palette: listed again on every show, so tab titles are root results. Three sources are merged:
- DevTools protocol: a Chromium browser (Chrome, Chromium, Brave,
Edge, Vivaldi, Arc) started with
--remote-debugging-port=<port>. The tab list ishttp://127.0.0.1:<port>/json; one WebSocket to the browser then asks every page whether a media element is playing, whether it is muted (theplayingandmutedtags), how far into its longest video it is, and which browser window it is in. Chrome 136 and later ignore the port flag on the default profile: it needs--user-data-dirtoo. - AppleScript (macOS): the browsers in
appsthat are running, over oneosascriptrun (JavaScript for Automation): Safari, and Chrome when nothing on the port already covers it. No playing or muted state this way. - Firefox: its session file (
sessionstore-backups/recovery.jsonlz4, the newest profile's, LZ4-decoded in the extension). Read-only, since Firefox has no scripting interface: its tabs list, and Focus can only raise its window.
The row is the tab's title (the url without its scheme when there is
none), the host is the subtitle, the favicon comes from the url; a tab
without a web url (chrome://settings) gets the browser's icon. On the
right: where it is in one run (the browser's name when more than one is
listed, window N when the browser has more than one window), and
playing (green) or muted for a tab the protocol could ask. The url,
the host and the browser's name are keywords. The details (⌘I) lead
with the browser and window over the title, sound as a chip, then the
address.
The filter dropdown scopes the list:
| filter | rows |
|---|---|
| All | everything (the default) |
| Audible | tabs a page reports as playing sound unmuted (DevTools tabs only) |
| This window | the front window of each browser: the window of the most recently used DevTools tab, window 1 over AppleScript, Firefox's selected window |
Actions:
| action | shortcut | what |
|---|---|---|
| Focus | Enter |
selects the tab in the browser (/json/activate plus Page.bringToFront, or set active tab index and set index of window to 1), then hides and raises the browser window through the focus effect (the window carrying the tab's title, else the browser's front one); for Firefox, "Focus window" raises its window only |
| Close | ⌘W |
closes the tab (/json/close, or the tab's close over AppleScript); not for Firefox |
| Copy URL | ⌘C |
copies the url |
| Mute / Unmute | ⌘M |
DevTools tabs only: sets muted on every media element in the page, so it is the page's sound, not Chrome's tab mute |
| Copy as markdown link | ⌘⇧C |
copies [title](url) |
| Play on Apple TV | ⌘T |
a YouTube video, while the Apple TV extension is installed and on (suggests): from where the tab is once its video has started ("from 12:34", the url's t set to it), else the url as it is; pal://appletv/play?url= plays it and pauses the tab |
| Play on Apple TV from the start | ⌘⇧T |
the same, a started video only, its t and start dropped |
Close and mute keep the palette open and list again. A focus, close or mute that fails keeps the panel open with a toast carrying the reason.
Permissions and platforms:
- Automation (macOS): the first AppleScript run asks whether pal may control Safari (and Chrome); a refusal makes every later run fail with error -1743, which the palette shows as one row, "Automation permission needed", whose action opens Privacy & Security, Automation. The DevTools and Firefox tabs still list under it.
- Nothing on the port, no scriptable browser running and no Firefox session is one row saying so.
- Linux: the DevTools and Firefox sources only.
Settings, [extensions.browser-tabs]:
| key | type | default | what |
|---|---|---|---|
port |
number | 9222 |
The --remote-debugging-port to look at. |
apps |
list | ["Safari", "Google Chrome"] |
macOS: browsers asked over AppleScript when running, by app name. A Chromium browser already on the port is skipped. |
firefox |
bool | true |
List Firefox tabs from its session file. |
firefox_session |
path | `` | A recovery.jsonlz4 to read; empty finds the most recently written profile's. |
GitHub (github-prs, github-issues, github-repos, github-notifications, github-search)
One extension, five palettes, one sign-in; multi, so a second account is
[instances."github@work"] with its own token under
[extensions."github@work"] (Config).
| palette | id | kind | what Enter does |
|---|---|---|---|
| Pull Requests | github-prs |
indexed, 5 min | opens the pull request |
| Issues | github-issues |
indexed, 5 min | opens the issue |
| Repositories | github-repos |
indexed, 5 min | opens the repository on GitHub |
| Notifications | github-notifications |
live, 1 min | marks the thread read and opens it |
| Search GitHub | github-search |
input | opens what was found |
Signing in. The token setting when set (a personal access token
with repo, notifications, read:org; the file holds a keychain:
reference, never the value), else the gh CLI's login (gh auth token,
asked at most every five minutes). Neither one gives a single hint row
per palette saying how to sign in; Enter on it opens the token page. A
token GitHub rejects is the same row with GitHub's message.
Requests. Every request has a 10 s timeout. Pull Requests, Issues and
Search are one GraphQL request each, the filters' queries as aliases in
it, so "All" and each filter come from the same answer; Repositories and
Notifications are REST, which carries ETags: a re-list sends
If-None-Match and a 304 costs nothing against the rate limit. The
rows already normalised, with the ETag, are kept in the extension's
storage (never a raw body), so a restart still has them and an outage
shows the last rows with a note in the log. While the rate limit is
exhausted a hint row at the top says when it resets.
Pull Requests. Yours (is:open author:@me), the ones waiting on
your review (review-requested:@me), and yours merged within
merged_days; filters (Tab) All, Mine, Review requested, Merged; the row is
the title with owner/repo #n under it, a state dot (green open, grey
draft, violet merged, red closed), tags for the checks (checks ✓ /
✗ / … from the status rollup), the review decision (approved,
changes requested, review), draft, conflicts, and the updated date. The
pane shows the body, then the latest comments and reviews, over the
repository, author, branch, size, every check by name, who approved and
who asked for changes, reviewers, labels and dates (fetched when the
cursor rests on the row).
| action | shortcut | when |
|---|---|---|
| Open | Enter |
|
| Copy URL | ⌘C |
|
| Checkout branch | ⌘⇧O |
open, gh on PATH, a clone under repos_root (gh pr checkout there) |
| Copy branch name | ⌘B |
|
| Open checks | ⌘⇧K |
|
| Open files changed | ⌘⇧F |
|
| Copy reference | owner/repo#n |
|
| Mark ready for review | ⌘⇧R |
a draft |
| Merge | ⌘⇧M |
open, not a draft, GitHub says mergeable; asks first; merge_method |
Issues. Assigned to you, mentioning you, opened by you (open ones);
filters (Tab) All, Assigned, Mentioned, Created; an issue in two lists is
listed once, in the first. Rows carry the first two labels, the comment
count and the updated date; the pane the body, the latest comments and
the milestone. Actions: Open, Copy URL (⌘C), Copy reference, Close
(⌘⇧X, asks first). Create Issue at the top is a form: the
repository (a select of the repositories with recent activity in the
lists, then yours by push date), title, body; the submit opens the new
issue.
Repositories. Yours (owner or collaborator, by push date), your
default_org's recently pushed, and your starred ones, in that order and
sectioned so; filters (Tab) All, Mine, Starred, Organisation. Rows: name,
description, tags for private, archived and the language, stars, the
push date; the pane adds forks, open issues, the default branch, the
clone url and the local clone when one sits under repos_root
(<root>/<name> or <root>/<owner>/<name>). Actions: Open on GitHub,
Open in editor (⌘E, with a clone: code when on PATH, else the folder
with the system opener), Open folder (⌘O), Copy clone URL (⌘⇧C, ssh
or https per clone_protocol), Copy URL, Copy owner/name, Open issues,
Open pull requests. Create Repository is a form (owner: you or the
organisation, name, description, private).
Notifications. Unread threads, sectioned by reason (Review requested,
Mentioned, Assigned, Your threads, Comments, State changed, CI, Security,
Subscribed) and newest first inside one; the first row is the unread
count (its actions: open the inbox, mark all read). A row's url is the
subject's page (pull, issue, commit; a release or discussion falls back to
the repository's page). Enter marks the thread read and opens it, so the
count is honest when you are back; Mark as read (⌘⇧R) does not open;
Mark all as read (⌘⇧A) asks first. The bar item github/notifications
shows the unread count as a badge (hidden at zero) over the same cache;
its popover has every unread thread (it scrolls), Open all (this palette)
and Mark all read.
Popover keys of github/notifications (rendered every 300 s and on show, wake,
network; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
enter |
Mark the thread read and open it on GitHub |
m |
Mark the thread read |
a |
Mark all read |
p |
Open the Notifications palette |
cmd+c |
Copy the thread's URL |
up |
Move between threads (or j and k) |
Search GitHub. GitHub's own search syntax, typed: free text,
repo:owner/name, is:pr, author:login, label:bug. Results come
nearest first, each once: Involved (involves:@me: you opened it, are
assigned, were mentioned or commented), Your organisations (your
account and every organisation you belong to), Repositories (yours
first), Everywhere, Users. A query naming its own repo:, org: or
user: skips the organisations tier, and its remaining matches are
Other matches. Your pull requests and issues already cached by the
two palettes above are also matched loosely against the query, so a typo
GitHub's word search misses (directry) still finds them at the top of
Involved, shown as you type, before GitHub is asked. Each tier is its own
request, sent together, and each shows as soon as the ones above it are
in, below what is already there; the whole search takes about as long as
GitHub's slowest (about 2 s), and the search row's sweep runs until it
lands. A pull request found this way comes without the fields
GitHub computes per result (checks, review decision, conflicts, size,
which tripled the wait); one that is also in your cached lists shows
that copy, tags and all, and the pane fetches the checks by name. Filters Everything, Issues and PRs,
Repositories, Users; a query carrying is:, repo:, author: or the
like searches issues and pull requests only. The rows and actions are the
palettes' above; users show their avatar and open their profile. A
keystroke waits 300 ms for the next before asking. Give it an alias
([palettes.github-search] alias = "gs") and gs <text> at the root
lands in it with the text typed.
Settings, [extensions.github]:
| key | type | default | what |
|---|---|---|---|
token |
secret | (none) | A personal access token; empty means the gh CLI's login. |
default_org |
text | (none) | The organisation whose recently pushed repositories are listed, and an owner the Create Repository form offers. |
repos_root |
path | (none) | Where your clones live; enables Checkout branch and Open in editor. ~ is expanded. |
clone_protocol |
ssh / https |
ssh |
What Copy clone URL copies. |
merged_days |
number (days) | 7 |
How far back the Merged list reaches. |
merge_method |
merge / squash / rebase |
merge |
How the Merge action merges. |
prs item settings, [bar.items."github/prs".settings]:
| key | type | default | what |
|---|---|---|---|
review_requests |
boolean | false |
Count the reviews asked of you on the Pull requests strip; off, the strip is your own pull requests and a review sits in the popover alone. |
For the tests, PAL_GITHUB_API points the extension at another base url
(a local server) and PAL_GITHUB_TOKEN is used as the token without
asking gh.
Docker (docker, docker-images, docker-compose)
Three live palettes over the docker CLI (docker ps -a, docker images,
docker compose ls -a, each with --format '{{json .}}'), listed again
on show once their rows are older than ttl. Podman works through the
same commands: set binary to podman, or install its docker alias.
Without the binary, or with the daemon not answering, each palette is one
inert hint row that says which.
Docker Containers (docker): every container, the running ones first
in a Running section, the rest under Stopped. The row is the container's
name, its image the subtitle; on the right the published ports, compact
(80, 443; 8080:80 when host and container differ; a stopped one's
status, Exited (0) 3 days ago, instead) and a state tag (running green,
exited grey, paused and restarting amber, created blue, dead red). The id,
the image and the Compose project are keywords; the detail pane leads
with the project and image, the name, the state and status as chips, then
lists id, command, created, ports, mounts and networks.
| action | shortcut | what |
|---|---|---|
| Stop / Start | Enter |
docker stop for a running container (after a confirm), docker start for a stopped one; the palette lists again with a toast |
| Logs | ⌘L |
docker logs --tail 200, stdout and stderr, as a show level in a code fence |
| Shell | ⌘T |
opens a terminal running docker exec -it <id> sh -c '…' (bash when the image has it, sh otherwise); running containers only |
| Restart | ⌘⇧R |
docker restart |
| Remove | ⌘D |
docker rm -f, after a confirm |
| Copy id | ⌘C |
copies the short id |
Docker Images (docker-images): repository:tag (the id for an
untagged image), the id as subtitle, size and age on the right. Run
(Enter) asks for a name and ports in a form (host:container pairs,
comma separated; a malformed one is refused under the field) and runs
docker run -d [--name] [-p …] <image>; Copy id (⌘C); Remove (⌘D,
docker rmi, after a confirm).
Compose Projects (docker-compose): every project docker knows of,
its folder as subtitle, its containers' states as one tag (4 running, 1 exited; running green, a mix amber, exited grey). Up (Enter, compose up -d), Logs (⌘L, compose logs --tail 200), Restart (⌘⇧R), Down (⌘D, after a confirm), Open
project folder (⌘O). Every config file of the project is passed with
-f.
The panel gives a pick 10 s. A command still running after 8 s (a stop
whose process ignores SIGTERM and waits for docker's 10 s kill, a compose up that pulls) is left to finish on its own and the toast says so; ⌘R
later shows the outcome. A failed command keeps the panel open with
docker's last stderr line.
Which terminal Shell opens is the same rule as SSH Hosts' Connect: the
terminal setting on macOS, $TERMINAL then kitty, foot, alacritty,
xterm on Linux.
Settings, [extensions.docker]:
| key | type | default | what |
|---|---|---|---|
binary |
text | docker |
The CLI to run; podman works. |
ttl |
number (s) | 10 |
How long a listing stays good for before a show lists again. Palette meta, read when the extension loads. |
terminal |
auto, kitty, Terminal, iTerm2, Ghostty, Alacritty |
auto |
macOS only: which terminal Shell opens. |
Services (services)
The OS's service manager, one live palette. What it lists depends on the
platform; ttl is the same "list again on show once older than" as
Docker's.
Linux, systemd. Filters (Tab): User (systemctl --user), System, Failed
and Running (the last two span both managers, in a User and a System
section). The row is the unit without .service, its description the
subtitle; on the right the unit file's state (enabled, disabled;
nothing for static, generated and transient units) and a tag with the
active state: the sub state alone when active (running, exited,
green), active/sub otherwise (inactive/dead grey, failed/failed
red, activating amber). Units are what list-units --all has loaded,
then the unit files it has not (a disabled service that never ran),
inactive and without a description; templates, masked and alias files
are skipped. An instance (app-foo@autostart.service) shows its
template's enabled state.
| action | shortcut | what |
|---|---|---|
| Stop / Start | Enter |
systemctl stop for an active unit, start otherwise |
| Logs | ⌘L |
journalctl -u <unit> -n 200 --no-pager (--user for a user unit) as a show level |
| Restart | ⌘⇧R |
systemctl restart |
| Enable / Disable | ⌘E |
flips on the unit file's state; absent for a static, generated or transient unit |
| Copy unit name | ⌘C |
copies name.service |
A system unit's Stop, Restart and Disable ask first (a user unit's too
with confirm_user). A system verb runs as systemctl --no-ask-password <verb>; when that is refused for want of authentication it is tried
through sudo -n (a credential window opened by sudo -v in a
terminal), then pkexec (a polkit agent's prompt, when one is running).
When none is allowed the panel stays open with a toast quoting all three
refusals. A verb still running after 8 s (a stop waiting on
TimeoutStopSec, a polkit prompt) goes on without the panel; ⌘R shows
the outcome. A journalctl for a system unit shows what your user may
read.
macOS, launchd. Filters: Agents (the plists in the agent_dirs
folders, one section per folder), Loaded (everything launchctl list
knows, sorted; the application.* jobs of running apps skipped) and
Running (those with a pid). The row is the label, the program from the
plist (its Program, or the first ProgramArguments entry) the
subtitle, and on the right the pid and a tag: running (green), loaded
(blue, no process right now), exit N (red, the last exit was not 0),
not loaded (grey, a plist launchd has not been given). A binary plist is
read through plutil.
| action | shortcut | what |
|---|---|---|
| Unload / Load | Enter |
launchctl bootout gui/$UID/<label> (after a confirm) for a loaded job, launchctl bootstrap gui/$UID <plist> for one that is not |
| Restart | ⌘⇧R |
launchctl kickstart -k gui/$UID/<label> |
| Show plist | ⌘L |
the plist as XML in a show level |
| Open plist file | ⌘O |
opens the file |
| Copy label | ⌘C |
copies the label |
PAL_SERVICES_BACKEND=systemd|launchd forces a backend; the tests run
both on one machine against fake binaries.
Settings, [extensions.services]:
| key | type | default | what |
|---|---|---|---|
ttl |
number (s) | 10 |
How long a listing stays good for before a show lists again. Read when the extension loads. |
confirm_user |
bool | false |
Ask before stopping, restarting or disabling a user unit too. |
agent_dirs |
list | ~/Library/LaunchAgents, /Library/LaunchAgents |
macOS only: where agent plists are read from. |
Makefile Targets (make)
Every target of every Makefile under the projects folders, one section
per project. A folder is scanned two levels deep for a Makefile,
makefile or GNUmakefile (GNU make's own order when several are
there); dot folders and node_modules, target, vendor, dist,
build are not entered. Targets are read from the file's text, not from
make -pn: a line beginning with one or more names and a colon
(:=-style assignments, pattern rules and $(VAR) targets are not
targets), .PHONY names included even when their rule is not literal,
in file order. A description is the ## text on the rule's line, else
the comment line right above it (a .PHONY: line in between is skipped).
The row is the target, its description the subtitle (the project folder
without one), the project's name a keyword (so pal test finds it from
the root); a phony target has phony as one too. The detail pane leads
with the Makefile's path, the target and a phony chip, then the
description and the recipe.
| action | shortcut | what |
|---|---|---|
| Run | Enter |
see below |
| Copy command | ⌘C |
copies make -C <dir> <target> |
| Open project | ⌘O |
opens the project folder |
| Show Makefile | ⌘L |
the whole file in a show level |
Run opens a terminal in the project folder running make <target>; the
window stays open until Enter is pressed, so a quick target's output is
not gone with it, and the HUD names the target. With terminal = "background" make runs unseen instead: a toast carries the exit status
and the output's last line, and a failure opens the whole output in a
show level too; a run still going after 8 s is left to finish on its
own and the toast says so. The terminal is chosen as for SSH Hosts'
Connect.
Settings, [extensions.make]:
| key | type | default | what |
|---|---|---|---|
projects |
list | ~/proj |
Folders to scan, two levels deep. ~ is expanded; a folder that is not there lists nothing. |
terminal |
auto, background, kitty, Terminal, iTerm2, Ghostty, Alacritty |
auto |
Where Run puts make. |
Audio (audio)
Every audio output and input, in two sections, over the core's audio
capability. The default of each direction carries a default tag, the
volume is the accessory, a muted device is tagged muted; the subtitle
says the direction and, when the OS reports it, the transport
(bluetooth, usb, builtin). A headset shows once under Output and
once under Input. Live: read again on every show. With no audio backend
the one row says which tool is missing.
- macOS: the CoreAudio HAL, in-process (no tool to install): the device's UID is its id (stable across replugs), the volume is the virtual main volume the menu bar slider moves, with the per-channel scalar for a device without one; a digital output without a volume control has no volume accessory and no Mute action.
- Linux:
wpctl(PipeWire), elsepactl -f json(PulseAudio 16+).
| action | shortcut | what |
|---|---|---|
| Set as Output / Set as Input | Enter |
makes the row the default for its direction; the HUD names it |
| Set Volume… | ⌘⇧V |
drills into a level of presets, 0 / 25 / 50 / 75 / 100 %, the current one tagged; a pick sets it and the HUD says so |
| Mute / Unmute | ⌘M |
toggles mute and stays in the list |
No extension settings. volume item settings, [bar.items."audio/volume".settings]:
| key | type | default | what |
|---|---|---|---|
level |
select | flash |
where the bar's percentage lives: flash for three seconds after a change, always, or never |
Bluetooth (bluetooth)
The paired devices over the core's bluetooth capability, connected ones
first then by name, each with a glyph for its kind (headphones, speaker,
keyboard, mouse, game controller, phone, computer), a connected tag and
the battery as the accessory when the OS reports one (AirPods show
L 80% · R 75% · Case 90%). Live: read again on every show. With no
adapter the one row says so.
- macOS: the list is
system_profiler SPBluetoothDataType -json(the one unprivileged source with the device type and the battery levels, ~80 ms); connect and disconnect are IOBluetooth's own calls (whatblueutildoes), so nothing has to be installed. - Linux: BlueZ over
bluetoothctl(devices Paired,info,connect,disconnect), each call time-boxed, and only with an adapter under/sys/class/bluetooth(without onebluetoothctlhangs rather than answering empty).
| action | shortcut | what |
|---|---|---|
| Connect / Disconnect | Enter |
toggles, on the state read back from the OS at that moment; Disconnect asks first; the HUD says which happened |
| Copy Address | ⌘C |
copies AA:BB:CC:DD:EE:FF |
No extension settings. battery item settings, [bar.items."bluetooth/battery".settings]:
| key | type | default | what |
|---|---|---|---|
low_threshold |
number (%) | 25 |
a connected device at or below this level counts as low: named on the bar item, and its rules show the item |
Privacy (privacy)
What is using the camera, the microphone or the screen right now, over
the core's privacy capability: one row per app, what it holds and for how
long (from the log line that first named it, else from when pal first saw
it). Live: read again on every show, so camera or mic at the root
answers who has it.
- macOS: the camera and the screen from what drives the menu bar's
dots, Control Center's
sensor-indicatorslog (Active activity attributions changed to ["cam:us.zoom.xos", "scr:com.google.Chrome"]): the last line of the past day, thenlog streamfor every change, each bundle id named by its app (System, macOS's own capture, reads macOS). The microphone per app from CoreAudio's process objects (kAudioProcessPropertyIsRunningInput, macOS 14.2+), named by the app responsible for the process (ffmpegin kitty reads kitty, ffmpeg in the subtitle). A camera CoreMediaIO sees running before the log names it is the device alone; an attached Screen Sharing session (screensharingd) is Screen Sharing. - Linux:
pactl -f json list source-outputsfor the microphone (pavucontrol's peak meters left out),/proc/*/fdlinks into/dev/video*for the camera.
| action | shortcut | what |
|---|---|---|
| Show the app | Enter |
brings the app forward; a row with no app opens its privacy settings |
| Open privacy settings | ⌘, |
the sensor's pane of Privacy & Security |
The bar item Camera, Mic & Screen (privacy/in-use) is off the strip
until something is in use, then one glyph per sensor on an amber band,
the apps in the tooltip; its popover is the same rows, each sensor a
coloured glyph and word, the durations ticking while it is open. The core
watches (every second on macOS, two on Linux, and at once on a Control
Center log line, from the first time the extension asks) and fires the privacy trigger on a change, which the
item's refresh.on names; the extension does not poll. No settings.
Wi-Fi (wifi)
The network the machine is on, the saved ones and what is in range, over
the core's wifi capability, in sections Current / Known / Available plus
a Wi-Fi row that turns the radio off or on. The current row's subtitle
has the IP, channel and security, its accessories the signal (▂▄▆█ 92%)
and a connected tag; a known network that a scan also sees carries its
signal and an in range tag; an available one shows its security
(Open for none) and channel. Live: read again on every show.
- macOS: CoreWLAN in-process for the interface, the radio, the
current link (name, signal, channel, security) and a scan;
networksetupfor the preferred list, join, forget and the radio switch;ipconfig getsummaryfor the IP;security find-generic-password -wa <ssid>for a password (the keychain prompts; that dialog is yours to answer). A scan takes several seconds (the radio walks every channel; CoreWLAN andsystem_profilertake the same 9 s on an M-series laptop), so Available shows the last one (kept for a minute) and the Scan for Networks row runs a fresh one. macOS 15 and later show network names only to an app with Location Services (CoreWLAN answers nil, the CLIs<redacted>;wdutil infowould say, but needs sudo), so the first time the palette lists with the names withheld while you are inside it, it asks, once: the system prompt, the same as Calendars (a listing at startup or on a show is held back, so nothing asks at first run or next to another prompt; the Wi-Fi names need Location access row asks onEntermeanwhile). Say yes and the next listing has the names; until then the current row reads "Connected network" with its IP and channel, and the Scan row counts the nearby networks whose names are hidden. A refusal keeps that row (Enteropens Privacy & Security > Location Services) and puts a Grant button in Settings > General > Permissions and on the Overview. - Linux: NetworkManager over
nmcli -t(device wifi list, which answers from NetworkManager's own scan cache, so Available lists at once;connection showfor the saved ones;device wifi connect;connection delete;radio wifi;-s -g 802-11-wireless-security.pskfor a password).
| action | shortcut | what |
|---|---|---|
| Join | Enter |
a saved or open network joins at once; a secured new one asks for its password in a form, and a refused join shows the form again with the tool's message |
| Copy Password | ⌘⇧C |
the saved password onto the clipboard |
| Copy IP | ⌘C on the current row |
the interface's IPv4 address |
| Copy Name | ⌘C on an available row |
the network's name |
| Forget | ⌃X |
removes the saved network, after a confirm |
| Scan | Enter on the Scan row |
a fresh scan, then the list again |
| Turn Wi-Fi Off / On | Enter on the Wi-Fi row |
the radio; off, only that row is listed |
| Allow Location Access / Open System Settings | Enter on the Location row (macOS) |
the system prompt while unasked, the Location Services pane once refused |
No settings.
Now Playing (media)
The playing track is also the empty root's Now section's row (nothing while
nothing plays). One row per running player over the core's media capability: the
track as the title, artist and album as the subtitle, the cover (the system's
Now Playing artwork on macOS, else the player's artwork url, else the app's
icon), the position as 12:34 / 1:06:03, the player's name and a playing /
paused / stopped tag, playing ones first. A player with nothing loaded reads
"Nothing playing" with the player's name; one playing without a track (Chrome
with YouTube on macOS reports the position and nothing else) is its app's name
with the position as the subtitle. Live: read again on every show, and the
position is the core's estimate at that moment (it advances from the last report
and the clock while playing). With no player running the one row says so; on
Linux without playerctl it says to install it.
- macOS: Spotify and Music through AppleScript, only while the app is
running (the check is
NSRunningApplication, so pal never launches one to ask) and only once macOS lets pal automate it: the first Apple Event to an app is the system's Automation consent alert (Music adds its Media Library one), and pal never fires that from a listing or at launch. Until you allow it the app's track shows on the system-wide row below, and the palette has a row "Spotify is running; let pal control it directly" whose Enter lets macOS ask; refused in System Settings > Privacy & Security > Automation stays refused. Spotify's own row gives the artwork url and the track url. The system-wide Now Playing (any other player: a browser, VLC) is one more row, named and iconed from the app's bundle id, through the bundled MediaRemote adapter (mediaremote-adapter.plplus a framework, run by/usr/bin/perl; the one source that works on macOS 15.4 and later, wherenowplaying-cligets null). The adapter runs as onestreamchild of pal for as long as pal does (started on the first look, restarted if it dies, ended with pal): every change the system reports lands in memory as it happens, with the cover, so a look costs microseconds and never a process. Nothing to install. - Linux:
playerctlover MPRIS, one row per player; the icon is the player's.desktopwhen one is named like it. - Your other extensions' players (the
playercontrol,docs/design/controls.md): what the Apple TV, Spotify (its Web API, so any device) and Jellyfin play comes first, named by its app and device ("YouTube · Living room"), the position moved along from when it was read. Enter opens that extension's own view (the Apple TV's remote, Spotify's lyrics),⌘Enterplays or pauses,⌘→/⌘←skip, each on that player. A player that says it shows the same playback as a system row (Spotify on this Mac) drops that row. The bar item leaves alone a player whose extension's own item is on the strip (Spotify's lyric line, the Apple TV's remote), so one playback is never on the bar twice and no bespoke item is replaced; the rest it follows as before.
| action | shortcut | what |
|---|---|---|
| Play / Pause | Enter |
toggles and stays in the list, so the tag follows |
| Next Track | ⌘→ |
|
| Previous Track | ⌘← |
|
| Copy Track | ⌘C |
copies artist - title |
| Open in … | ⌘O |
the track's url (Spotify's spotify:track: link), else the app on macOS |
The bar item media/now-playing puts the playing track on the strip
(hidden while nothing plays) with the track row (the cover, or the app's
icon, at row size; a click opens the track) over Pause, Next, Previous,
Copy Track and Open in its popover. On macOS the core's stream fires the
item's media trigger the moment the track, the state or the cover
changes, so the strip follows a skip at once; on Linux the extension
polls the players every 5 s while one plays and pushes a track change
itself.
Popover keys of media/now-playing (rendered every 30 s and on show, wake,
media; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
space |
Pause or play |
right |
Next track (cmd+right too) |
left |
Previous track (cmd+left too) |
c |
Copy artist - title (cmd+c too) |
o |
Open the track in its player (cmd+o too) |
Settings, [extensions.media]:
| setting | default | what |
|---|---|---|
Leave to another extension (exclude) |
empty | players this extension leaves to another, by app name or player id (Spotify, music): the bar item and the root's Now row skip them; the palette still lists them |
now-playing item settings, [bar.items."media/now-playing".settings]:
| setting | default | what |
|---|---|---|
Cover on the bar (artwork) |
off | the cover instead of the note on the menu bar strip, only when the cover is square (a 24 pt picture of anything else is a smudge); the popover shows the cover either way |
The Playing card (media/playing, a glance-only item: "strip": false, in the default general.glance): one card for whatever plays,
published players and the system's alike, a provider's own bar item
showing it or not (the card is no strip, so nothing doubles). The one
that started playing last leads: its track (the app when it names
none), "artist · where" under it, its cover, its progress along the
foot, rendered every 10 s while it plays. A second playing one is an
"also: Weird Fishes · Spotify" line, a third and more "+N more"; with
nothing playing the last paused one leads, muted; nothing at all is no
card. The card's click and its ⌥N open the lead's own view (the Apple
TV's remote, Spotify's lyrics, this palette for a system player); an
also line opens that player's, "+N more" this palette.
Displays (displays, displays/brightness)
Every display with its mode and brightness, from displays/.
One row per display: its name (the Displays pane's, "Built-in Liquid
Retina XDR Display", "DELL U2720Q"), the mode it is in as the subtitle
(2560×1440 @ 60 Hz · HiDPI, the rotation, the connection), main and
mirror / mirrored tags, and the brightness as the accessory where
something can read it. The typed argument on the row takes a percent or
a step (40, +10) for ⌘B. Enter drills into the display:
- Brightness, Contrast, Volume (the last two over DDC, on an
external monitor; Volume only when the monitor answers for it): a
slider view.
←→move by the step setting,⇧←⇧→by 1 %,1…9are 10…90 %,0is 100 %,mfull, a click on the slider sets the clicked fraction. The built-in panel never goes below 5 % from here, so the screen stays readable. From the row,⌘=and⌘-step without opening the slider and⌘Btakes the percent typed in the bar. - Input source: HDMI 1 / 2, DisplayPort 1 / 2, USB-C (the VCP 60
codes; LG's alternate codes by the setting), or another code typed in.
ddcctlandddcutilread which is active and tag it;m1ddconly sets. - Resolution & scaling: every mode displayplacer lists, HiDPI ("looks like") first, then native, each with its refresh rate, the current one tagged. A mode that changes the resolution or the scaling asks first; the arrangement before is kept as an Undo row at the palette's root until it is used or forgotten.
- Rotation (0 / 90 / 180 / 270; rotating the built-in screen asks, with displayplacer's own warning), Mirror with… / Stop mirroring, Make main (the origins shift so the display sits at (0,0), the layout otherwise kept), Sleep displays (System's own command).
At the root too: Night Shift on / off when the nightlight CLI is
installed, Sleep displays, and the presets: "Save current
arrangement as…" keeps displayplacer's own reproduce line (one command
per output on Linux) under a name; each preset is a row with Apply,
Rename (⌘R), Overwrite with the current arrangement (⌘S) and Delete
(⌘⌫), its commands in the detail pane. Live: read again on every show
(the tool listings are cached for 20 s, ⌘R reads afresh). What is
missing for the displays there are is a Setup row each, Enter copying
the install command.
| tool | macOS | Linux | what it adds |
|---|---|---|---|
| nothing | system_profiler |
hyprctl / wlr-randr / xrandr, whichever is there |
the displays, their names, the current mode, main and mirror |
displayplacer (brew install displayplacer) |
modes, rotation, mirroring, make main, presets | the compositor tool does these | arrangement |
brightness (brew install --HEAD brightness; the bottled 1.2 cannot read Apple Silicon panels) |
the built-in display and Apple displays | brightnessctl |
brightness |
m1ddc (Apple Silicon) / ddcctl (Intel) |
external monitors over DDC/CI | ddcutil |
brightness, contrast, volume, input source |
nightlight (brew install smudge/smudge/nightlight) |
Night Shift | no | the toggle |
Settings, [extensions.displays]:
| setting | default | what |
|---|---|---|
Brightness step (step) |
5 % | what the arrows, a scroll on the bar item and a +10-style link move by |
LG input codes (input_alt) |
off | the alternate VCP 60 addressing LG (and some others) take |
brightness item settings, [bar.items."displays/brightness".settings]:
| setting | default | what |
|---|---|---|
Display on the bar (display) |
the external display | whose level the bar item shows: external, main or builtin; a pal://displays/brightness link naming no display acts on it too |
Bar item displays/brightness: the chosen display's brightness as a
glyph (three steps of the same sun) and the percent; a scroll on the item
moves it by the step. The popover is a card per display with its mode
line and a slider (↑ ↓ move between them, ← → and the digits set
the one the cursor is on, n Night Shift, Enter opens the display in
pal, p the palette). Hidden by its rules (Settings > Bar) while only the
built-in display is connected, whose keyboard keys already set it, and
while nothing installed can set any display; show = "always" keeps it
muted with the level.
Routes: pal://displays/brightness?value=50|+10|-10&display=main|external|builtin|<id>|<name>
(also contrast, volume), pal://displays/input?source=hdmi1|hdmi2|dp1|dp2|usbc|<code>,
pal://displays/mode?mode=<n>|1800x1169|1800x1169@60|2560x1440 hidpi|3024x1964 native,
pal://displays/preset?name=<name>, pal://displays/night-shift?state=on|off|toggle.
Unicode Characters (unicode)
A grid of 1795 characters one pastes rather than types (a catalog at
the root: three rows there, the rest behind the "more" row), from
unicode/data.json: arrows, math, Greek, currency, quotes and
dashes, punctuation, typographic and zero-width spaces, superscripts and
fractions, accented Latin letters (the Turkish, German, French, Nordic and
Spanish sets), letterlike symbols, the Mac keyboard glyphs (⌘ ⌥ ⇧ ⌃ ⎋ ⏎ ⌫
⇥ ⏏ and the control pictures), check marks, box drawing and block
elements, geometric shapes, miscellaneous symbols, dingbats, enclosed
numbers and letters. The tile is the glyph (a space shows as the open box
␣); the name is the Unicode name in lower case; the section is the
block. The search matches the name, the code point (2192 or u+2192),
the HTML entity names (rarr, nbsp), the LaTeX command where there is an
obvious one (\rightarrow, \alpha), the character's Unicode 1.0 name
(command key) and plain words (cmd, turkish, tick, eur).
The last twelve characters copied lead the grid in a Recent section (kept in the extension's storage) and leave their block while they are there. The section is rebuilt when the palette lists again (at start, on ⌘R); between listings the core's frecency already moves a picked tile up.
Actions:
| action | shortcut | what |
|---|---|---|
| Copy character | Enter |
copies the glyph |
| Paste | ⌘Enter |
hides and pastes it into the app in front (needs Accessibility on macOS, like Snippets) |
| Copy code point | ⌘⇧U |
U+2192 |
| Copy HTML entity | ⌘⇧E |
→, or → for a character without a named entity |
| Copy numeric reference | ⌘⇧N |
→ |
The detail pane (⌘I) shows the glyph large, the block, the code point,
both HTML forms, the UTF-8 bytes (E2 86 92), the LaTeX command and the
aliases.
The table is generated: bun run unicode/build.ts fetches
UnicodeData.txt (names) and the WHATWG entities.json (entity names),
applies the sections, LaTeX and keyword tables in the script, and writes
data.json (160 KB), which is committed. The whole UCD is not shipped;
add a range or a code point to the script's SECTIONS to widen the table.
Settings, per palette, [palettes.unicode.settings]:
| key | type | default | what |
|---|---|---|---|
columns |
number, 4 to 16 | 10 |
Tiles per row in the grid. Read once when the extension loads, like Emoji's. |
Colors (colors-picker, colors, colors-history, colors-convert)
Four palettes over one colour maths (the SDK's colors, sdk/src/color.ts) and one
history. At the root a hex, an rgb()/hsl()/oklch()… notation or a CSS name
answers inline under a Convert Colour section (the first four notations; Enter
opens the picker on it). Colour Picker is a view level: a large swatch on a
sunken well over a hue strip and a saturation/value plane (both drawn by the app
from the colour, the ring marking where it sits), the colour in hex, rgb, hsl,
hwb, oklch, oklab, lab and display-p3 with its CSS name (or the nearest, in
OKLab), the nearest Tailwind and Material tokens, the contrast on white, on
black and against the previous colour with the WCAG level as a badge, and to the
right nine tints, nine shades and the complementary, analogous, triadic, split
and tetradic harmonies as tiles. The keys edit the colour in place: ←/→ turn
the hue 5°, ↑/↓ move the lightness 2 points, -/+ the saturation 5
points, ⇧ makes an arrow step three to five times bigger, m switches to
OKLCH (lightness, chroma and hue; a chroma step stops at the sRGB gamut edge
instead of clipping) and back. ⇥ moves the focus from the swatch to the tints,
the shades and each harmony row in turn (⇧⇥ back): the arrows then walk the
row, the label names the tile and Enter takes it. A digit or # opens a text
field in the search row with that character; type any notation and Enter
applies it, Escape closes the field (a notation that does not parse stays in
the field with a toast). c (and Enter on the swatch) copies in the notation
the format setting names, ⌘C always the hex, ⌘⇧R rgb, ⌘⇧H hsl, ⌘⇧O
oklch, ⌘⇧L lab, ⌘⇧P display-p3, ⌘⇧N the CSS name, hwb and oklab from ⌘K.
p picks from the screen: the panel hides, the system's loupe appears
(NSColorSampler on macOS, the screenshot portal on Linux), and the panel comes
back in the picker on the picked colour (unchanged after Escape). h opens the
history, n the named sets, u goes back to the previous colour, r makes a
random one. The colour, the model and the history live in the extension's
storage, so Escape and a restart lose nothing.
Named Colours is a grid of 995 swatch tiles (a catalog at the root:
three rows there, the rest behind the "more" row), one section per set,
with a filter per set in the dropdown (⇥ cycles them): the 148 CSS
names, Tailwind 3.4 (slate 500), the Material 3 baseline tonal palettes
(primary 40, generated from the seed #6750A4 with Google's
material-color-utilities), the 2014 Material palette (red a200), Apple's
system colours in the light and the dark appearance (blue, gray6),
Catppuccin's four flavours, Rosé Pine's three variants, Nord, Solarized,
and pal's own tokens. The search matches the name, the hex, the token
spelling (slate-500, systemBlue, mocha/mauve) and the set. Enter
opens the tile in the picker; ⌘Enter copies it in the chosen notation,
⌘⇧C the hex, ⌘⇧N the token. The detail pane (⌘I) adds where the
token is used: the Tailwind utilities, the Material 3 role, the
UIColor/NSColor name and what Apple uses it for, the role a theme gives
it (Catppuccin's mauve is keywords, Nord's nord8 the primary accent).
The last twelve opened lead in a Recent section. The sets setting
narrows what All lists; the dropdown reaches every set regardless.
Colour History (live, so its rows at the root follow every pick and
copy) lists every picked and copied colour, newest first, one row per
colour, with the hex, where it came from (the screen, typed, a
set with its token, the picker, the converter) and when; the CSS name (or
≈ the nearest) is the accessory, the chosen notation when it is not hex.
The first row is Pick Colour from Screen, which is also at the root:
the loupe, then the colour copied in the chosen notation with the HUD
naming it, and remembered. Enter on a colour opens it in the picker,
⌘Enter copies it, ⌘D removes it, ⌘⇧D clears the history (with a
question). The history_size setting caps it (50).
Convert Colour is an input palette: type a colour in any notation and
the rows are its conversions, each with a swatch. It reads hex in every
length with or without the hash (#f80, ff880080), rgb()/rgba() in
the comma and the space syntax with percentages and alpha (rgb(255 136 0 / 50%)), a bare triple (255 136 0), hsl()/hsla() with hue units
(deg, turn, grad, rad), hwb(), oklch(), oklab(), lab() (CIE
Lab against D50, as CSS means it), color(display-p3 …), color(srgb …)
and the CSS names. The rows: every notation, the CSS name (or the nearest
one, tagged close, near or far by its OKLab distance), and the
contrast ratio on white and on black, each tagged with what it passes for
normal text (AAA at 7, AA at 4.5, AA large at 3, else fail, WCAG
2). Enter opens the picker on the colour, ⌘Enter copies the row; a
contrast row only copies. Something that is not a colour gives one inert
row saying so.
The rows of the sets are generated by bun run colors/build.ts
(the CSS table from color.ts, the tokens read from tokens.css,
Tailwind and Material fetched from unpkg, Material 3 computed with the
library from esm.sh, Catppuccin from its GitHub palette; Apple, Rosé Pine,
Nord and Solarized are hand-kept in sets.ts) into data.json (72 KB),
committed. The maths is unit-tested against the CSS Color 4 reference
points (test/colors.test.ts in pal-extensions).
Settings, [extensions.colors]:
| key | type | default | what |
|---|---|---|---|
format |
hex, rgb, hsl, hwb, oklch, oklab, lab, p3, name |
hex |
What c, Copy and the grid's ⌘Enter write. |
uppercase |
boolean | false |
#FF8800 rather than #ff8800. |
alpha |
keep, drop |
keep |
Whether a translucent colour copies with its alpha (#ff880080, rgba(…), / 0.5) or as the opaque colour. |
sets |
list of set ids | every set | Which sets the grid lists under All: css, tailwind, material3, material, apple, catppuccin, rosepine, nord, solarized, pal. |
history_size |
number, 5 to 500 | 50 |
How many colours the history keeps. |
Per palette, [palettes.colors.settings]:
| key | type | default | what |
|---|---|---|---|
columns |
number, 4 to 16 | 8 |
Tiles per row in the grid. Read once when the extension loads. |
Icons (icons, icons-freedesktop, icons-iconify)
The two glyph grids are catalogs at the root: three rows each there, the rest
behind the "more" row, and a glyph named exactly what was typed (git)
sits under the primary rows that have the word, not above Google Chrome.
Nerd Font icons is a grid of every glyph in the Symbols Nerd Font the
app bundles (app/src/assets/fonts, Nerd Fonts 3.5.1): 10995 glyphs, one
section per set in a fixed order (Material Design, Font Awesome, Codicons,
Octicons, Devicons, Seti, Weather, Font Logos, Font Awesome Extension,
Powerline, Powerline Extra, Pomicons, IEC Power, Custom, Extra, Indent).
The tile is the glyph, the name is the set's name with spaces (account circle), the code point is the subtitle and the nf-md-account_circle
name a keyword, so md-account or f0009 finds it. The last twelve
picked lead in a Recent section.
| action | shortcut | what |
|---|---|---|
| Copy glyph | Enter |
the character itself, as an icon for a script or a bar |
| Copy code point | ⌘Enter |
U+F0009 |
| Copy name | ⌘⇧N |
nf-md-account_circle |
| Copy CSS class | ⌘⇧C |
nf nf-md-account_circle |
The detail pane names the set and the Nerd Fonts version, the code point,
the CSS class and the \u{f0009} escape; it does not draw the glyph large,
since the pane's text is the UI font and only the icon box uses the
bundled symbols font.
Freedesktop icon names is a second grid: the 114 freedesktop names the SDK's
xdg() maps to a glyph (sdk/src/icons.ts, what a script's icon_xdg may
say), each drawn with its glyph and its Nerd Font name as the subtitle. Enter
copies the name; the glyph (⌘Enter) and the code point (⌘⇧U) are the other
actions. Only the names in that table are listed: pal draws no other, so a
longer list would show names that render as nothing.
Iconify Icons is a third grid, an input palette over Iconify's public
API (api.iconify.design, no key): every set it hosts (Material Design
Icons, Tabler, Lucide, Phosphor, Simple Icons, Heroicons and the rest,
some 200k icons), searched by name 250 ms after the last keystroke:
/search?query= answers the names and the sets, then one
/<prefix>.json?icons= per set the SVG bodies, cached per word and per
icon for the process. Each tile is the icon's own SVG, filled mid-grey so
it reads on both themes (Iconify draws in currentColor, which a picture
cannot inherit); the set's name is the section. Nothing typed lists
nothing (the placeholder says what to type); no hit, a rate limit (429)
or an unreachable API is one hint row, and a failed search is not cached
so ⌘R asks again.
| action | shortcut | what |
|---|---|---|
| Copy SVG | Enter |
the whole file, currentColor kept |
| Copy name | ⌘Enter |
mdi:home |
| Copy as data URL | ⌘⇧D |
data:image/svg+xml;utf8,..., what an { image } icon or a CSS background takes |
| Open on Iconify | ⌘O |
the icon's page on icon-sets.iconify.design |
| Save SVG… | ⌘S |
<prefix>-<name>.svg into the save_to folder, -2 when the name is taken; the HUD names the path |
The glyph table is generated: bun run icons/build.ts fetches
glyphnames.json at the pinned release and writes data.json (282 KB,
[name, code] pairs by set), committed; bump the version in the script
together with the font. A listing is eleven thousand rows (about 3.4 MB
over the host's pipe) on every start, as the palette has no ttl; the
rows carry the least they can for that.
Settings, [extensions.icons]:
| key | type | default | what |
|---|---|---|---|
sets |
list | [] |
Set prefixes the Iconify search is limited to (mdi, tabler, lucide, phosphor, simple-icons). Empty: every set. |
save_to |
path | ~/Downloads |
Where Save SVG puts an Iconify icon. ~ is expanded. |
Per palette, [palettes.icons.settings] and [palettes.icons-iconify.settings]:
| key | type | default | what |
|---|---|---|---|
columns |
number, 4 to 16 | 10 (Iconify 8) |
Tiles per row in the grid. Read once when the extension loads. |
Night Parade (night-parade)
A survivors game: the Night Parade of One Hundred Demons marches through
an abandoned village tonight, and you last until dawn. Enter on the
palette's row opens it as a view level whose body is the extension's own
page (a surface). The design, and why each part is there, is
night-parade/DESIGN.md.
- A night is fifteen minutes in three acts (Dusk, Midnight, the Hour of the Ox), each with its own crowd and scripted events, mini-bosses at 2:30, 7:30 and 12:30 and bosses at 5:00, 10:00 and 15:00. Beat the Oni and dawn breaks.
- Weapons fire on their own. The arrows (or WASD) move,
spacedashes (you can't be hurt in a dash), and each level offers three choices (four with luck):123or the arrows and Enter;Rrerolls,Xskips,Bbanishes, each with charges from the shrine. - Sixteen weapons, each with eight levels and an evolution when you carry its item; sixteen items; six heroes, four of them unlocked by playing. Your attacks break the enemy shots they touch.
- Enter,
Por ⌘K's Pause stops the night; the pause card shows your build and stats,Qgives the night up. Escape leaves (the panel hidden pauses it); Backspace is back on the page's own screens. - Between nights: gold buys lasting blessings at the shrine, the codex
keeps what you've found and defeated, and harder omens open after the
first dawn. The save is the extension's storage; sound and music are
the page's own settings (
Mmutes).
Network (network)
This machine's addresses, one row per value: the value is the row's name
so it reads at a glance, the label is the subtitle, Enter copies. Live,
listed again when the panel shows and the last listing is over a minute
old; ⌘R lists now. The palette opens with the detail pane showing.
| section | rows | from |
|---|---|---|
| This machine | every interface's IPv4 and global IPv6 addresses (loopback and link-local left out), the Wi-Fi one labelled with its SSID; the Tailscale IPv4 and IPv6; the hostname; the Bonjour .local name on macOS |
macOS: ifconfig, networksetup -listallhardwareports (the kind), ipconfig getsummary <dev> (SSID, security); Linux: ip -j addr, iw dev; tailscale ip where the CLI is on PATH (or the Tailscale.app binary on macOS); os.hostname(), scutil --get LocalHostName |
| Internet | the public IP, with the city, country and organisation when the endpoint gives them | one GET of public_ip_url with a 3 s timeout, kept 10 minutes (⌘R fetches again; a failure is not kept); JSON with ip (ipinfo), query (ip-api) or connection.isp (ipwho.is) shapes, or a bare address |
| Network | the default gateway with its interface; the DNS servers in resolver order | macOS: route -n get default, scutil --dns; Linux: ip -j route show default, /etc/resolv.conf (behind systemd-resolved's stub, resolvectl dns) |
Every tool runs with a 3 s timeout and a missing one just leaves its rows
out. The tunnel interface carrying the Tailscale addresses (utun4,
tailscale0) is folded into the Tailscale rows. When nothing has an
address, or there is no route, an inert row says so; the public IP row
says why when the fetch fails.
The SSID on macOS: since Sonoma the system redacts the network name
for a process without Location Services access (ipconfig getsummary
prints <redacted>, networksetup -getairportnetwork says not
associated), and pal's own grant does not reach the tools it runs:
ipconfig under a granted pal still prints <redacted> (checked on
hornet). So when the summary is redacted the palette asks the core's wifi
capability, which reads the name in-process (CoreWLAN) and has it once
pal holds Location access (the Wi-Fi palette asks for it); until then the
Wi-Fi row is labelled by kind (en0 · Wi-Fi) and the detail pane says
what is missing.
Actions:
| action | shortcut | what |
|---|---|---|
| Copy | Enter |
copies the value |
| Open Network settings | ⌘Enter |
the Network pane of System Settings on macOS; on Linux the first of gnome-control-center network, systemsettings kcm_networkmanagement, nm-connection-editor installed |
No Flush DNS: on macOS it is dscacheutil -flushcache; killall -HUP mDNSResponder, which needs sudo, and the host has no way to ask for a
password; run it from a terminal.
The detail pane lists every field of the row: interface, kind, SSID, security, IPv4, IPv6, MAC and status for an address; city, region, country, organisation and when it was fetched for the public IP; the resolver order for a DNS server.
Settings, [extensions.network]:
| key | type | default | what |
|---|---|---|---|
public_ip_url |
text | https://ipinfo.io/json |
The endpoint the public IP row asks. Empty: no Internet section. https://api.ipify.org (a bare address) and http://ip-api.com/json work too. |
networks |
list | [] |
One line per network, SSID = kind label:Name icon:X (or gateway = ...): hide drops the network/status item, hotspot and public mark it, label: renames it on the bar, icon: replaces its glyph there and on the interface's rows here. An extension setting, since the palette's rows read the icon too. |
Timer (timer-timers)
Named countdown timers over the timer CLI (~/.local/bin/timer in the
owner's dotfiles): the CLI keeps the timers, one detached process per
timer that fires on its own (confetti, a chime, a phone ping) and one KV
file per timer under its state directory. pal is a view of that directory
and asks the CLI for every change, so a timer started from a terminal and
one started here are the same thing. Live: listed again on every show;
the empty root's Now section shows the most urgent one (running, paused
or just landed) with its own actions, nothing when there is none.
One row per timer, most urgent first (landed, then running soonest first,
then paused): the name, what is left and when it lands (or "Paused at
12:34", "Landed 0:42 ago"), a state tag. Then New timer, a
form: a duration (25m, 90s, 1h30m, 2:30, a bare number is minutes;
the CLI parses it and its complaint comes back under the field), an
optional name (the duration otherwise; a name already taken restarts that
timer), and a checkbox to ring the phone out loud when it lands (--ring).
The same from a link: pal://timer/start?duration=25m&name=tea
(&ring=1 rings; Links).
Start Pomodoro is the last row while no pomodoro runs. It starts a
work timer (Pomodoro 1 of 4, pomodoro_work minutes) and the cycle
then runs itself: when that timer lands the extension stops it and
starts the break's (Break 1 of 4, pomodoro_break), then the next
round, after the last round the long break (pomodoro_long_break), then
round 1 again, each a timer of the CLI's own (its chime and phone ping
fire as for any timer), each announced on the HUD ("Pomodoro. Break: 5
min"). The pomodoro's row wears an apple, the phase as a tag (work
violet, break green) and "Round 2 of 4, work" before what is left; the
bar item reads 18:27 · 2/4 while working and 4:59 · break on a break.
A finished work round counts toward Pomodoros today: N, an inert row
at the bottom (sixty days of counts in storage); the session is in
storage too, so a host restart picks it up. Stopping the pomodoro's
timer (here, or timer stop in a terminal) ends the cycle; so does a
landed one the CLI reaped while the host was down.
Actions, by state:
| row | Enter |
⌘+ |
⌘S |
⌘⇧D |
⌘D |
|---|---|---|---|---|---|
| running | Pause | Add 5 minutes | Stop | ||
| paused | Resume | Add 5 minutes | Stop | ||
| landed | Dismiss (the CLI's done) |
Add 5 minutes (restarts it) | Stop | ||
| the pomodoro's timer | as above | as above | Skip to the next phase (this phase's timer stopped, the next started; a skipped round is not counted) | Stop pomodoro (the cycle over, the timer gone) | Stop |
Every pick runs the CLI (timer pause <id>, resume, add 5m <id>,
stop <id>, done) with the state directory as TIMER_DIR and lists
again; a refusal is a toast with the CLI's words.
The bar item (timer/timer,
Extensions): the soonest
timer's remaining time as the title with a fill for how far along it is, blue,
then amber past two thirds, red past nine tenths, muted while paused; a landed
timer is the alarm (its name, or "Done" for an unnamed one, in red) until the
CLI's badge ttl (5 minutes) or a Dismiss removes it. Hidden with no timer at
all. A click opens this palette. The second-level countdown is pushed by the
extension itself (a watch on the state directory plus a 1 Hz tick while a timer
runs); the core asks every 10 s and on wake besides.
Popover keys of timer/timer (rendered every 10 s and on wake; the arrows move
the cursor, a click sets it):
| keys | does |
|---|---|
space |
Pause, resume or dismiss the timer with the ring |
+ |
Add five minutes |
backspace |
Stop |
up |
Move the ring to another timer (or click a card) |
n |
Start one: type 25m tea in the field, Enter |
p |
Start a pomodoro (while none runs) |
s |
Skip to the pomodoro's next phase (while one runs); cmd+shift+d stops the cycle |
o |
Open the Timers palette |
A pomodoro's card carries the phase as a tag next to when it lands. The
key hints are two rows, the card's keys then the popover's own, and the
second ends with today's finished rounds (3 today).
Settings, [extensions.timer]:
| key | type | default | what |
|---|---|---|---|
command |
path | timer |
The CLI, a name on PATH or a path. |
dir |
path | ~/.local/share/timer |
Its state directory (TIMER_DIR); made if missing. ~ is expanded. |
pomodoro_work |
number, minutes | 25 |
A work round. |
pomodoro_break |
number, minutes | 5 |
The break after a round. |
pomodoro_long_break |
number, minutes | 15 |
The break after the last round of a cycle. |
pomodoro_rounds |
number | 4 |
Work rounds per cycle before the long break. |
Calendar (calendar-today, calendar-schedule, calendar-quick, calendar/upcoming)
Two live palettes, one input palette (Quick Add) and a bar item over one
source and one cache; Today
also suggests the empty root's Now section its row: the event the strip
speaks for (the current one, else the next inside horizon_hours) as a
Today row, Join first when it has a call; and in place of it, a row per
call about to start (Standup over starts in 3 min · Zoom · 10:00 – 10:30, then started 4 min ago): from call_lead minutes before it (5
by default) until ten minutes in or its end, the earlier start first when
two overlap, declined and all-day events never; Enter joins, ⌘C copies
the link, ⌘Enter opens the event.
Quiet (no sound, no notification), read against the clock on every show
of the panel, the events from the cache when that is under a minute old
(calendar/source.ts). The source is a setting: system is
the core's calendar capability (pal_core::calendar: EventKit on macOS,
so every account Calendar.app has is one store and one permission; khal
on Linux, read through its list --json); google is the Calendar API
v3 read directly per account; auto (the default) is google once an
account is listed, else system. Every reader goes through load,
which keeps the last window fetched (local midnight to days ahead, two
at least) and answers it while it is younger than the caller's allowance:
a palette show takes up to a minute, the bar's minute tick five, a
refresh nothing. A fetch that fails leaves the last events in place as
stale, so a source that is away costs the strip its freshness, not the
meeting.
Today is the day's events in time order, whatever their state: the
row is the title, the time range with how long (10:00 – 10:30 · 30 min · Room 4, All day), the calendar glyph tinted with the calendar's
colour, and the state as the first tag: in 12 min (in 2 h 5 min) in
blue, now, 25 min left in green, over in grey, today for an all-day
one; then declined / maybe, the head count, Join when there is a
call. Once no timed event is left today a Nothing else today row
names the next timed event's day (Next: Concert, tomorrow 19:00,
Next: Review, Sat 19 Sep 09:00; Nothing today on a day that never
had one) and tomorrow's rows follow under their own section. The bar's
popover opens the same palette with args: { rest: true }: the over
ones dropped and tomorrow always there.
My Schedule is the week: sections Today, Tomorrow, This
week (within seven days), Later (up to the days setting); events
that have ended are gone, an all-day one lasts until its midnight; the
current event carries a now tag, else the next one in 12 min (in 2 h further out, nothing past a day); the calendar's colour as the row's
dot, the head count, Join, declined or maybe. The last row is
New event on the system source.
A call is Google's conferenceData video entry point (else
hangoutLink), or the first Zoom, Google Meet, Teams, Webex, Jitsi,
Whereby or GoTo meeting link in the event's url (system), location, then
notes or description; Outlook safelinks are unwrapped and &
unescaped. A Zoom marketing page or a docs link does not count.
| action | shortcut | what |
|---|---|---|
| Join call | Enter |
opens the call link; first only when there is one |
| Open in Calendar / Open in Google Calendar | Enter (⌘Enter with a call) |
ical://ekevent/… into Calendar.app, the occurrence for a repeating event (macOS, system source); the event's htmlLink in the browser for a Google account |
| Copy conference link | ⌘⇧C |
|
| Copy event details | ⌘C |
title, when, where and the link as text; the primary action on Linux without a call |
| Delete event / Delete this occurrence | ⌃X |
asks first; on a repeating event only that occurrence goes; macOS, system source only (khal has no delete; a Google token may be read-only) |
| New event | Enter on the last row |
the form below; system source only |
| Add event | Enter |
Quick Add: the typed line as the event (below) |
| Grant access | Enter on the permission row |
the system prompt, or System Settings when it was denied |
The filter dropdown is one entry per calendar (read when the host loads,
so a calendar added later shows at the next start; a Google account's
are named by account, Personal (personal)). The detail pane is the
notes as markdown (a Google description as text, its HTML stripped; the
title when there are none) over when with the duration, the calendar and
its account, the location, the call or link, the organizer, every
attendee with their reply as a coloured tag, your own reply, and whether
it repeats.
The bar item (calendar/upcoming,
Extensions): the next
event's name as the title with the time as a segment after it (in 12m, 25m left while it runs), so the target's max_chars clips the name and never the
time; while one runs the next due under the same rules adds an in 8m segment
in its own colour, its name in the tooltip. Hidden when nothing timed starts
within horizon_hours (10), so a clear evening is a clear strip. The event is
the first that has not ended, timed unless hide_all_day is off, not declined
unless hide_declined is off, starting inside the horizon; a running one
counts until it ends (a click joins its call, not the next one's). Colour by
escalation, the boundaries inclusive: muted far off, amber from
warn_minutes (15) before the start, red from urgent_minutes (5), green
while it runs; sketchybar draws the same names through the bar module's colour
map. A dot badge says there is a call. The tooltip is the title, the time
range and the calendar. A click opens Today in the popover (the rest of today
and tomorrow, Enter joins). The core asks every five minutes and on wake, the
network coming back and the minute tick; a minute tick renders from the cache
(0.1 ms through the host in the tests; the count-down needs no fetch), every
other reason fetches (about 80 ms for a week from EventKit on hornet, 250 to 350
ms for two Google accounts whose token commands hop over ssh). A failed fetch
keeps the last item as stale; no cache and no source is hidden.
Popover keys of calendar/upcoming (rendered every 300 s and on minute, wake,
network; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
enter |
Join the focused event's call, else open it in Calendar (Google Calendar for a Google account) |
j |
Join the next call |
t |
Show or fold tomorrow |
o |
Open Calendar (the day's page on calendar.google.com for a Google account) |
r |
Refresh |
cmd+c |
Copy the focused event's details |
cmd+shift+c |
Copy the focused event's conference link |
up, down |
Move between the rows; a click on a row focuses it, a click on Join joins it |
Google. Each account is one entry of accounts, name = command:
the command prints an access token for the Calendar API on stdout (a
bare token, or the JSON an OAuth endpoint answers with access_token
and expires_in); pal runs it through sh -c, keeps the token in memory
until its expiry (30 minutes for a bare one, a minute's margin), mints
again once on a 401, and never writes a token anywhere: the command is
the secret's owner, so nothing secret sits in the config. gcloud auth application-default print-access-token after an application-default login with the calendar.readonly scope (documented, not verified
here), a keychain helper, a broker behind ssh (quote the remote command
for its shell: ssh archer "curl -s 'http://…/token?aud=calendar'").
An entry reads the account's primary calendar; a table in the file, { name, token_command, calendars = ["primary", "<id>"] }, reads more.
Events are read with events.list (singleEvents, timeMin/timeMax,
the conferenceData entry points), the calendars from calendarList
(their colour, title and access role), all accounts at once. One account
away is logged and the rest list; every account away keeps the last
events read behind a hint row. Google rows open in the browser and are
not deleted or created from here.
On Linux, khal new prints no id and reads dates in the formats of its
own [locale] section; with none set (the C locale's %c), khal cannot
parse a timed event back, so pal refuses one with the format to set
(datetimeformat = %Y-%m-%d %H:%M) rather than saving it wrong. All-day
events go through either way.
New event is a form: a title; a day in words (today, tomorrow,
fri, next mon, next week, 2026-09-20, 20.9, 20 sep, sep 20);
a start and an end time (14:30, 2pm, 2:30 pm, 1430, noon; an end
before the start is the next day); an all-day checkbox (then the end may be
the last day, 22.9); the calendar (the writable ones, or the backend's
default); a location; notes. A field that does not parse shows its
complaint and keeps what was typed; the backend's refusal (a read-only
calendar) comes back under the title.
Quick Add Event (calendar-quick, an input palette) is the same
write from one typed line, parsed as you type (calendar/quick.ts,
pure, on parseDay and parseTime): a title first, then in any order a
day (tomorrow, fri, next tue, 20 sep, 2026-09-20, on monday),
a time or a range (10:00, 2pm-3pm, 14:00 to 15:30, 9-10am), for 45m (else default_length, 30 minutes), at <place> (free text at the
end, unless a time follows the at), @ <calendar> anywhere (in <calendar> too when a writable calendar starts with the word; an in
in a title stays), all day. No time makes it an all-day event; no day
means today, or tomorrow once the time has passed (an amber tomorrow
tag says so). The one row reads the event back, dentist · Fri 18 Sep 14:00 to 15:00 · Room 4 · Home calendar, the calendar's colour on the
glyph and the start as a date accessory; a line that is only a day, or
nothing, is a hint row naming the grammar; a calendar name nothing
matches falls to the default and the row says so. Enter creates it
through the form's write (calendar.create, the cache dropped, the
"Added" toast); a refusal is a failure toast. The palette declares
fallback: "Add “{query}” to the calendar", so a root query nothing
matched offers the line as an Add row. The permission rows are the
others'; a Google source refuses the write as the form does.
Permission. macOS lists an app under Privacy & Security > Calendars
only after it has asked once, so while the state is not_determined the
palette is one row, Grant calendar access, whose Enter shows the
system prompt (the panel hides while it is up; open the palette again
after answering). denied and restricted are one row that opens that
pane. The app carries NSCalendarsFullAccessUsageDescription (and the
pre-14 NSCalendarsUsageDescription) in its Info.plist; without the
string macOS ends the process instead of asking. On Linux without khal
the one row says so. The Google source needs no permission: a token that
fails shows on the listing.
Settings, [extensions.calendar]:
| key | type | default | what |
|---|---|---|---|
source |
select | auto |
auto, system, google. |
accounts |
list | [] |
Google accounts as name = command, or tables { name, token_command, calendars } in the file. |
calendars |
list | [] |
Calendar names (or ids, work:primary for Google) to list; empty is every calendar. Also narrows the filter dropdown. |
days |
number | 7 |
How many days from today My Schedule lists. |
hide_declined |
boolean | true |
Leave out invitations you declined. |
horizon_hours |
number | 10 |
The bar item shows the next event only when it starts within this many hours (the root's Now row too). |
hide_all_day |
boolean | true |
The bar item speaks for timed events only (the root's Now row too). |
call_lead |
number, 1 to 30 | 5 |
How many minutes before a call starts the root's Now row offers to join it (it stays until ten minutes in). |
default_length |
number, 5 to 480 | 30 |
How long a Quick Add event lasts when no end or for is typed (minutes). |
upcoming item settings, [bar.items."calendar/upcoming".settings]:
| key | type | default | what |
|---|---|---|---|
near_minutes |
number | 60 |
The item's near phase starts this many minutes before the event. |
warn_minutes |
number | 15 |
The item turns amber (warning) this many minutes before the event. |
urgent_minutes |
number | 5 |
The item turns red (critical) this many minutes before the event. |
Not built: accept and decline (EventKit has no public API to change a
participant's status; Raycast does it through the private
EKParticipant setter; the Google tokens this is built for are
read-only), a second calendars palette toggling visibility
(an extension cannot write its own settings), reminders, an OAuth flow
of pal's own (a Google client id would have to ship with it).
Flashcards (flashcards, flashcard-packs, flashcard-add)
Spaced repetition for the minute a build takes (flashcards/,
its README has the detail). Enter on Flashcards opens a view level
whose body is the extension's own page (a surface) on the next card due
across every pack in practice; each answer is saved as it is given, so
Escape at any point loses nothing. The first open asks what to learn.
- A card on a stack: Space flips it to the meaning and an example
sentence;
→(or Space) Knew it throws it right,←Didn't know left, or drag it; each button says when the card comes back (four buttons withbuttons: Barely and Too easy too,1–4). FSRS schedules (ts-fsrs, desired retention 90% by default). - Sessions of
sessionanswers (10) with a bar along the top, then a summary: the words missed, new and mastered as chips, the share of everyday speech the known words cover and what the session added; Enter keeps going,ddrills the misses,sthe stats page (streak and best, heatmap, the week ahead, each pack). - A pack with
reversemakes the back-to-front card of a word once the word is learned, and it is typed: case, punctuation and one of several meanings are fine; a missing accent, article or one slip is close. Tab gives a letter;a'types á,n~ñ. - New cards in pack order (
new_per_day, 15), one after every three reviews; the two cards of a word never on the same day; the day runs to 4 am. A noun's article is coloured by gender. ?lists the keys; Tab says the word (a deck's recording, else the best system voice installed); ⌘Z undoes, from the summary too; ⌘E writes your own meaning; ⌘K: suspend, learn more, the refresher (the cards missed this week, lapsed three times or fading, drilled without touching the schedule), mute, the packs folder. While cards are due, a row in the root's Now section.
Flashcard Packs lists the bundled packs (Spanish: Jeff Doozan's 6001
most common words with example sentences, everyday phrases) and the files in
the packs folder (an Anki deck as .apkg, with its recordings; TSV, CSV, an
Anki plain-text export, or JSON), what is
practised first, each with mastered, learning and new; Enter studies one
alone, ⌘Enter adds it to practice or takes it out, ⌘R its refresher.
Add Flashcard takes front = back into My cards. Browse Anki
Decks searches AnkiWeb's shared decks as you type (nothing typed: the
language being learned): cards, audio, pictures, how many liked it; the
description and sample cards in the pane; Enter downloads the deck in the
background into practice, ⌘Enter opens it on AnkiWeb. The first open's
b, a row in Flashcard Packs and ⌘K lead there.
| key | type | default | what |
|---|---|---|---|
new_per_day |
number | 15 |
New cards a day across the packs in practice. |
session |
number | 10 |
Answers a session, then the summary; 0 runs until nothing is due. |
goal |
number | 20 |
Answers a day that fill the ring and keep the streak. |
retention |
0.8, 0.85, 0.9, 0.95 |
0.9 |
Desired retention: how much of what comes back you should still know. |
typing |
reverse, always, never |
reverse |
Which cards are typed. |
speak |
auto, key, off |
key |
Say the word when it shows, only on Tab, or never. |
voice |
text | empty | A system voice by name; empty picks one for the pack's language. |
buttons |
two, four |
two |
Didn't know and Knew it, or Barely and Too easy too. |
sounds |
bool | true |
A soft tone per answer and for the goal. |
suggest |
bool | true |
The due row in the root's Now section. |
Games (games)
Every game as a showcase: the game the cursor is on at the top, its own
store screenshot cut to the game itself, its title and tagline beside
it, and every game's cover under it in a grid of five: two rows in view
and the top of the next, sliding up a row as the cursor goes down. A cover
is the part of a store screenshot the game picked to show itself up close
(cover, docs/design/screenshots.md). What came out in the last week comes first, newest first, marked New
(the listing's released, docs/registry.md); then the installed games,
then every game the registries list for this machine that is not
installed, marked Not installed; each of those by name. Nothing is
kept here: every open reads the installed extensions' manifests and lists
each view palette of a loaded extension the store shelves under Fun.
rightortabwalks to the next game,leftorshift+tabback, round the ends;downandupmove a row, in the same column; a letter or a digit jumps to the next game starting with it; a click on a cover goes to it. The title line says where you are ("3 of 15"), and the next open lands on the same game.enteropens the game as its own level (Escape comes back). On one not installed it installs it from its registry, waits for it to load, then opens it (Installing… meanwhile); an install that fails says why on the game until the next try.cmd+lon a game with leaderboards opens them.
The pictures are each game's cover shot (else its first), its -dark twin
(docs/registry.md), downloaded once into pal's cache folder
(~/Library/Caches/pal/games, ~/.cache/pal/games on Linux) and fetched
again after a week; a game whose picture is not there yet shows its
glyph until it lands. No settings.
2048 (2048)
The sliding-tile game in the panel, keyboard only. Enter on the palette's row (or its hotkey) opens the board as a view level: the search input gives way to the score, the footer shows the primary key, ⌘K lists every move with its key.
- Moving: the arrows or
hjklslide every tile that way; equal neighbours merge once per move, a 2 (or a 4, one in ten) lands on a free cell. A move that changes nothing spawns nothing and is not counted. Utakes the last move back (one move,undosetting), game over too.- The first 2048 shows a banner: Enter keeps going,
Nstarts over. Game over shows the score, Enter for a new game.Nmid-game asks first. - Escape leaves at any point; the board, the score, the move count and the best score persist (in the extension's storage).
Tiles are the view vocabulary's tile nodes on a sunken well, drawn by
the app with its tokens (paper for 2, the neutral tint for 4, then solid
tiles walking the tag palette's hues warm to cool, grey at 1024, the
accent at 2048), so the board follows the theme; a slid tile
glides to its new cell, a merge pops in place, the spawned tile pops a
beat later. The arrows and hjkl are one action each with two keys.
[extensions.2048]
undo = true
Wordle (wordle)
The five-letter word game in the panel, keyboard only. Enter on the palette's row (or its hotkey) opens the board as a view level: the search input gives way to the puzzle's name ("Daily #259", "Practice"), the footer shows the primary key, ⌘K lists the moves with their keys (the letters route without being listed).
- Typing: the letter keys fill the row, Backspace takes one back, Enter
submits; a word not in the list is a red badge over the board. A typed
letter pops in; a submitted row flips tile by tile: green in place,
amber elsewhere in the answer (a repeated letter only as often as the
answer has it), grey otherwise. The on-screen keyboard keeps each
letter's best mark. Tiles and keys are
tilenodes drawn by the app with its tokens, on a sunken well, so the board follows the theme. - Daily (
daily, on by default): one puzzle a day seeded from the local date, started when the palette opens on a new day. Once it is over,Nstarts a practice game on a random word; withdailyoff every game is one and⌘Nstarts another mid-game. - Hard mode (
hard_mode): greens stay in place, ambers must be used; a slip is named in the badge. A game keeps the mode it started with. - The result view: the praise or the answer, played, win %, streak and
best, the guess distribution as bars;
C(or Enter) copies the emoji grid with the puzzle number and the score. - Escape leaves at any point; the game and the stats persist (in the extension's storage).
The word lists ship with the extension, built from public domain sources (12dicts and ENABLE; the README says how): 2551 answers, 8878 allowed guesses, 69 KB in all. Nothing is fetched.
[extensions.wordle]
daily = true
hard_mode = false
Minesweeper (minesweeper)
The mine-clearing game in the panel, by mouse or one hand on the keys.
Enter on the palette's row (or its hotkey) opens the board as a view
level whose body is the extension's own page (a surface): the search
input gives way to the game's line ("7 mines left", "Cleared in 1:11"),
⌘K lists the moves.
- Moving: the arrows or
hjklmove the cursor (the ringed cell). - Opening: Enter or a click. The first open is always safe (the mines are laid after it, never on it or around it) and opens an area; an empty cell floods open. Enter or a click on a number whose flags are all placed opens around it (the chord, "open around" in the hint line); both buttons or the middle one do the same. A held button shows the cells it would open pressed.
- Flagging:
/,For Space, or a right-click; the counter is the mines less the flags. - The end: won when every safe cell is open (the mines left are flagged),
lost on a mine, which goes off red among the rest; a wrong flag is
crossed out. Enter or the face starts a new game;
Ndoes at any time, asking first mid-game. - The level:
Dopens the picker (left, right, Enter), or click a level; it writes thedifficultysetting. - One-handed: the arrows, Enter and
/(just above the arrows) play a whole game. - Escape leaves at any point; the board, the clock, the best time and the won count per level persist (in the extension's storage).
The clock starts on the first open and counts only while the board is on
screen: leaving the view pauses it, coming back resumes it, and a run pal
never ended (a quit, a crash) is cut off at the last move; T (or
clock = false) hides it while it keeps counting. The board is
the classic one redrawn: square raised cells that press flat, the classic
number colours tuned per theme, an LED counter and clock around the face.
An open lifts the caps ring by ring from the cell opened; a mine goes off
with a blast before the rest pop out; a cleared board plants its flags
under confetti. The cells scale to the page, so the 30 by 16 expert board
fits the compact panel.
[extensions.minesweeper]
difficulty = "beginner" # intermediate (16x16, 40), expert (30x16, 99); from the next board; D on the board too
clock = true # false hides the running time while you play (times still count); also T
Snake II (snake)
Snake II as the Nokia 3310 plays it, on the phone's own 84 by 48 screen.
Enter on the palette's row opens it as a view level whose body is the
extension's own page (a surface): the SNAKE II splash, then the phone's
menu (New game, Level, Mazes, Top score, Instructions), lit green, and the
game unlit, as the phone keeps its backlight. Every screen, sprite, maze,
rule, delay and tone comes from the real 3310 firmware (v6.07) run in an
emulator, and the tests replay 123 runs recorded from it frame by frame.
The panel's title line keeps the top score in view ("Top score 213"),
so it reads without the phone's Top score screen. That and a turn queue
(queue_turns, on: a second turn pressed within one step waits for the
next instead of being lost as on the phone) are pal's additions; the
README lists every one under "Not the phone".
- Steering: the arrows are the phone's 2 4 6 8 while playing; the digits,
*and#are the keypad (1 3 7 9 turn across the way the snake goes,*and#turn left and right). Only the last key before a step counts; a key back the way the snake came is dropped. - Enter is the phone's left soft key (Menu, Select, OK, More), Backspace its C; in a menu the up and down arrows are its scroll keys (Page Up and Page Down anywhere).
- Levels 1 to 9 (656 ms a step down to 88 ms), No maze and Mazes 1 to 5, food worth the level, a bonus creature after every fifth food with its countdown from 20, worth 5 x level + 5 + 2 x the countdown.
- A step into a wall or the body waits about 100 ms and tries again with any turn pressed meanwhile: a late turn still saves the snake.
- Game over blinks the snake, then the score; a new top score gets the fireworks and the jingle.
- C or Menu in play pauses (Continue first in the menu); Escape and the panel hiding pause too, and Continue is there next time, across restarts. C on the Snake II menu, or a digit on any of its screens (the phone's dialer), leaves the game and closes the view.
- The level, the maze, the top score, a paused game and the phone's random numbers persist in the extension's storage; the numbers start again from 1 when pal starts (the phone's power-on), so the first food after it is where the phone puts it.
[extensions.snake]
tones = true # the buzzer: eating, a crash, the top score's jingle; cmd+T on the game
Solitaire (solitaire)
Klondike in the panel, by mouse or one-handed on the arrows and Enter.
Enter on the palette's row (or its hotkey) opens the table as a view
level whose body is the extension's own page (a surface): a felt with
real cards, as large as the panel allows. The search input gives way to
what the cursor is on ("Pile 4 · 9♣ 8♥"), what you carry ("Moving 7♥ and
2 more") or why a move was refused ("7♥ can't go on 9♠"); ⌘K lists draw,
undo, finish and new game.
- The mouse: drag a card (and the cards under it) onto a pile; the piles it can go on light up, a drop anywhere else glides it back. Or click the cards, then the pile; a double-click sends a card where it goes, a click on the stock draws.
- The cursor:
←→walk the stock, the waste, the foundations and the seven piles (wrapping;hltoo); on a pile↑↓take more or fewer cards of its face-up run, and past it move to the row above or below (kjtoo). - Enter picks up what the cursor's ring holds; the cards ride on the pile under the cursor, and Enter drops them where it is. An illegal drop puts them back with the reason. Enter again on the pile they came from sends them where they go: the card's foundation, else the first pile that takes them. Any foundation takes a card onto its own suit's.
- Enter on the stock, or
space(d) anywhere, turns one card (three withdraw = "3") onto the waste, and the waste back over once the stock is out; passes are unlimited.Uor Backspace undoes (a hundred moves back, the turned-over card too), or puts down the cards held. - Once the stock and the waste are played out and every card is face up,
the rest flies home one card at a time (Enter finishes at once), and a
win bounces the cards off the foundations.
Ndeals a new game, asking first mid-game. - Escape leaves at any point; the game, the moves, the time and the
record (games won of played) persist (in the extension's storage). The
time counts only while the table is open; T (or
clock = false) hides it from beside the table, and the win still says how long it took.
Every card glides to where it goes and turns over with a flip, the deal goes out row by row from the stock, and a column fans out while it has room and tightens as it grows, never past where a card's rank shows.
[extensions.solitaire]
draw = "1" # or "3"
clock = true # false hides the running time while you play; also T
Vortex (vortex)
Turn round the centre and slip through the gaps as walls close in on the
beat, in the manner of Super Hexagon. Enter on the palette's row opens it as
a view level whose body is the extension's own page (a surface), drawn in
WebGL2 with its own synthesised music. The design, and why each part is
there, is vortex/DESIGN.md.
←→(orAD) turn; Shift turns slowly. Turning into a wall's side stops you against it; a wall reaching you ends the run.- Ranks at 10, 20, 30, 45 and 60 seconds; a minute clears a stage and opens the next and the stage's hyper. Six stages, each with its own song, colours, camera and patterns; the walls land on the song's beat.
- A stage is a chart, the same walls every run, written to its song; Endless runs every stage in turn with new walls each time, and the daily has the same walls for everyone all day (both open once Pulse is cleared).
- On the stages,
←→choose,↑picks a cleared stage's hyper,Ppractises from a rank you reached,Copens your look (shapes and trails opened by medals, the ghost on or off),spaceplays. After a death (and its slow replay),spaceis the next run and Backspace the stages;Mturns the sound off. - Escape (or closing pal) pauses a run;
spacecarries on. Each board's best, tries and time persist in the extension's storage. One setting,volume.
Yahtzee (yahtzee)
Solo Yahtzee: five dice, three rolls a round, thirteen rounds. Enter on
the palette's row (or its hotkey) opens the game as a view level whose
body is the extension's own page (a surface): the dice on a tray with
Roll beside them, the scorecard under them in two columns and the total
at the bottom, all of it in view at once, compact included. The title
line says the round and the roll ("Round 4 · Roll 2 of 3", "Choose a
category"); ⌘K lists Roll and every category the dice may go in, the one
that adds most first.
- Every open category shows what it would score with the dice as they lie. A picked row (hover, or the keys) rings the dice it counts, shows on the total what it would add (bonuses included) and, for an upper box, its share of the bar to 63.
- The keys, one hand:
space(orR, or↑on the dice) rolls;←→pick a die and Enter holds it (1to5hold directly);↓goes to the card, on the category that adds most, where the arrows move between the open ones (↑past the top is back to the dice) and Enter scores. After the third roll the cursor is on the card by itself.Nstarts a new game, asking first mid-game. - The mouse: click a die to hold it, Roll to roll, a row to score it.
- The dice tumble when thrown (held ones stay put), a score drops into its row while the total counts up, a Yahtzee gets a banner and confetti, and game over shows the final score with the best and the average.
- Escape leaves at any point; the game and the record (games, best, average) persist in the extension's storage.
Rules (Hasbro's): 35 for 63 or more in the upper section; a further Yahtzee pays 100 while the Yahtzee box holds 50, and plays as a Joker: its number's upper box if open, else any open lower box (a full house and the straights at full value), else an upper box for 0. No settings.
Typing (typing)
A typing test in the manner of monkeytype. Enter on the palette's row
opens it as a view level whose body is the extension's own page (a
surface): the options on a bar at the top, three lines of common
English words, a caret that glides from letter to letter. The title line
names the test ("Typing · time 30"); ⌘K lists a new test, every length
and mode, and the toggles.
- Time (15, 30, 60, 120 seconds), words (10, 25, 50, 100) or zen (type anything, Enter ends it), with punctuation and numbers on top; picked on the bar, with the arrows before the first key, or from ⌘K.
- Settings (also switched from ⌘K):
pace_caret(off,pb,average,last), a fainter second caret gliding at that speed to race;stop_on_error(off,letter,word), which refuses a wrong letter, or a space out of a wrong word;clock(on by default; ⌘T), the seconds left in a time test or so far in zen, which off hides (the test still ends on time, the result says how long it took). - The clock starts on the first key. Right letters turn full colour,
wrong ones red, extra ones trail, a word left wrong is underlined;
Backspace goes back into a word only if it was left wrong
(
alt+backspacetakes the whole word). - The result: wpm, accuracy, raw speed, consistency, the letters right, wrong, extra and missed, and a chart of the test second by second; a new personal best for that kind of test is marked, else the best and the average of the last ten. Under 75% accuracy a test is not counted.
- Stats and history (the
statsbutton on the bar,Son the result, or ⌘K): all tests or one kind, the totals and averages, a progress chart with the average of ten, the bests for every length and the table of past tests. - Tab starts a new test at any point,
Ron the result repeats the same words. Escape leaves (a test in progress is dropped); the options and the records persist in the extension's storage.
Crossword (crossword)
Daily crosswords: Crosshare's English minis (crosshare.org), in the manner
of the NYT Mini, and the Turkish papers' kare bulmaca (HaberTürk,
Cumhuriyet, Sabah). Enter on the palette's row opens a view level whose
body is the extension's own page (a surface): the grid, as large as the
panel allows; the puzzle's title and its constructor or paper (a link to
it on their site), the clock, the clue you are on in a bar of its own, and
the Across and Down lists. The title line says which puzzle it is
("Crossword · Today's mini", "Crossword · HaberTürk, today").
- Sources. Crosshare: its daily minis back to 2020 and its newest minis. HaberTürk: one 8 by 8 a day since November 2017. Cumhuriyet: a 17 by 11 with a photo the clues point at (a click shows it large), since February 2026. Sabah: 9 by 9s from an archive that stopped in April 2025. Each is fetched only when played or listed and kept; nothing of theirs ships with pal. The papers' days are Istanbul's.
- Which puzzle. The first open is the puzzle left half-done, else
today's of the
sourcesetting. Next (⌘N, or Enter on the finish) stays with the source: today's while it is open, then back through its days (for Crosshare, its newest minis too); a puzzle solved or started is skipped, and so are Crosshare's grids bigger than 7 by 7. The next one is fetched while you solve, so it opens at once. Browse (⌘O) has In progress first, every half-done puzzle from any source, the last played on top; then a tab per source: a calendar month by month, each day marked solved, started or new, and Crosshare's newest minis as a list; Enter plays (a half-done one where it was left). - The keys are the NYT's: a letter fills the square and moves to the
next empty one in the word (a full word typed over goes square by
square; a finished word jumps to the next clue with a gap); Backspace
clears the square, or steps back and clears; an arrow along the word
moves (over blocks), across it turns first; Space or a click on the
square turns; Tab and Shift-Tab (and Enter) walk the clues, skipping
full ones; a click on a square or a clue goes there.
?shows every key. - Turkish letters. A Turkish puzzle takes i as İ and ı as I, and a letter counts with its diacritics folded (S for Ş, C for Ç), as the papers' own players check: their grids cross Ç with C. A US keyboard solves one with plain letters, each shown as the answer's own once it matches (c typed where Ç goes shows Ç).
- Help. Check (⌘E the word, ⌘⌥E the square, ⌘G the grid, or Enter
on "Not quite") slashes
wrong letters and turns right ones blue (they lock); reveal (⌘U, ⌘⌥U,
⌘⇧U, the grid asking first) fills in the answer with a corner mark;
⌥⌫ clears the word, ⌘⌥⌫ the grid.
autocheckmarks a wrong letter as it is typed. A reveal makes the solve "finished with help": it counts, but not for the best time or the streak. - A wide grid (Cumhuriyet's) puts the clue across the top and the lists beside the grid; in the compact panel the lists give way, and ⌘L lays every clue over the grid.
- The clock runs only while the grid is on screen: it stops when the
panel hides, on the other screens and while paused (⌘P, or a click on
the clock), which covers the grid. ⌘T (or ⌘K, or
clock = false) hides it, pause button and all, for anyone a ticking time rushes; the time still counts for the stats and shows on the finish. - The finish. A full grid with a mistake says "Not quite" without saying where. Solved, a wave of light runs across the grid, and the time shows with a new best or the streak, the constructor's note if there is one, and Next puzzle.
- Stats (⌘S), per source: solved, best and average time, the streak (the source's daily solved on its own day, on the source's calendar) and the best one, a chart of recent times, the history.
- Every key is saved as you type (
progress.jsonin~/Library/Application Support/pal/crossword/,~/.local/share/pal/crossword/on Linux), so Escape at any point loses nothing. A puzzle once opened plays offline; offline, the page says so and lists them. - While today's puzzle of the
sourcesetting is unsolved, a quiet row in the root's Now section opens it, once you have solved a puzzle before (suggest).
[extensions.crossword]
source = "crosshare" # what the first open and the Now row offer: haberturk, cumhuriyet, sabah
autocheck = false # mark a wrong letter as it is typed; also on ⌘K
clock = true # false hides the running time while you play (times still count); also ⌘T
suggest = true # today's puzzle in the root's Now section while unsolved
Sudoku (sudoku)
Sudoku made on your machine, never fetched. Enter on the palette's row
opens a view level whose body is the extension's own page (a surface):
the board on the left, as tall as the panel allows; beside it a slim side:
the difficulty and the clock, a line filling as the cells do, a number pad
of slim keys (a bar under each digit for how many are down, dimmed with a
check once all nine are), and one row of tools: Undo, Erase, Notes, Auto
notes, Digit first and Hint. The title line says which puzzle it is
("Sudoku · Hard, today's").
- Which puzzle. A daily for each of Easy, Medium, Hard and Expert,
made from the date, so it is the same on every machine that day, and a
new puzzle of any difficulty whenever you like. The first open is the
game left half-done, else today's daily of the
difficultysetting. ⌘N opens a menu of the four: today's puzzle (Play, Carry on or Done) or a new one. Browse (⌘O) has every half-done game (Enter carries on) and a calendar of each difficulty's dailies, month by month, marked solved, started or new. - The grades come from solving the puzzle the way a person would, the simplest technique first, and taking the hardest one it needed: Easy is singles you can spot without notes, Medium needs a cell with only one digit left, Hard needs locked candidates or a pair or triple, Expert an X-wing, a swordfish or an XY-wing (and a few such steps). Every puzzle has one answer.
- Keys. 1 to 9 place a digit. A different digit typed over one you
placed turns the cell into notes holding both, and from then on digits
toggle notes there until Backspace clears the cell (changing your mind
never loses the first guess; Backspace then the digit replaces it). With
check = "mistakes"the answer is no secret, so a wrong digit is simply replaced and a right one stays. Shift or Option with a digit writes a note, and N (or Space) switches digits to notes, where the modifier places. Notes sit in a 3 by 3 inside the cell; a placed digit clears its note from every cell it sees. A fills in every note (notes you wrote are only trimmed, so your own eliminations stay), Shift-A clears them. Backspace or 0 erases, U or ⌘Z undoes, Shift-U or ⌘⇧Z redoes, the arrows orhjklmove, Tab goes to the next empty cell.?shows every key, and a newcomer (fewer than three solves) gets one line about the second digit until a few digits are down. - The pad (the mouse): a click places the digit on the cursor's cell (a note in notes mode); a right-click or ⌥-click writes the note instead, and so does each key's small corner, which lights when the cursor's cell has that note.
- Auto notes (C, or
auto_notes = true): every empty cell shows the digits that still fit, updated as you play; you only take notes out (Shift with a digit, the corner), and what you take out stays out as the board changes. Backspace on an empty cell brings its notes back, and turning auto notes off brings back your own notes as they were. - Digit first (D): pick a digit (1 to 9 or the pad) and every cell it can still go in is lit; a click places it (a note in notes mode), a click on it takes it out again, a click on another digit picks that one. Enter places it on the cursor's cell, Tab goes to the next lit cell, and once all nine are in it moves on to the next digit. D again leaves (Escape stays the panel's: it closes the game).
- Clarity. Clues solid, your digits in the accent colour. The cursor's
row, column and box are lit, every copy of its digit is lit and that
digit's notes are circled across the board. A digit twice in a row,
column or box gets a red line under it, with the clue it runs into
ringed; with
check = "mistakes"any digit that is not the answer shows in red, and the mistakes are counted. - Hints (I) come in three presses: where to look ("Look at row 4"), then why, in plain words, with the digit ("Only this cell in the top box can take a 9: every other empty cell sees a 9 already", the 9s circled), then the move (the digit placed, a wrong digit erased, or the marks updated). A wrong digit is always the first hint.
- The clock runs only while the board is on screen: it stops when the
panel hides, on the other screens and while paused (P or ⌘P), which
covers the board. T (or ⌘K, or
clock = false) hides it, pause button and all, for anyone a ticking time rushes; the time still counts for the stats and shows on the finish. - The finish. A completed row, column or box sweeps with light; a digit's ninth copy makes all nine pulse; the solved board ripples out from the last digit under confetti, with the time, a new best, the streak, Flawless (no hint, no mistake), and today's next daily or a new puzzle on Enter.
- Stats (⌘S), per difficulty: solved, best and average time (solves without a hint), the streak (that difficulty's daily solved on its own day, local time) and the best one, a chart of recent times, the history.
- Every move is saved (
progress.jsonin~/Library/Application Support/pal/sudoku/,~/.local/share/pal/sudoku/on Linux), so Escape at any point loses nothing.
[extensions.sudoku]
difficulty = "medium" # whose daily the first open shows: easy, medium, hard, expert
check = "conflicts" # or "mistakes": mark a digit that is not the answer as you place it; also on ⌘K
clock = true # false hides the running time while you play (times still count); also T
auto_notes = false # true keeps every empty cell's notes filled in; you only take them out; also C
Slack (slack-unreads, slack-channels, slack-search, slack-status)
One extension, four palettes and a bar item (multi: a second workspace
is [instances."slack@work"], its workspace set for that instance,
Config), signed in through the Slack
desktop app's own session (or a user token).
| palette | id | kind | what Enter does |
|---|---|---|---|
| Unreads | slack-unreads |
live | opens the conversation in the Slack app, at the message |
| Channels | slack-channels |
indexed, 1 h, catalog | opens the conversation in the Slack app |
| Search Slack | slack-search |
input | opens the message in the Slack app |
| Status | slack-status |
live | sets the status, snoozes, or flips presence |
Signing in. auth = "app" (the default) reads the desktop app's
session: the xoxc- token per workspace out of its Local Storage (a
LevelDB: the SSTables and the write-ahead log are parsed, both copied
first) and the d cookie out of its cookie jar (AES-128-CBC under the
"Slack Safe Storage" password from the login keychain, or the Secret
Service / "peanuts" on Linux). Read-only, kept in memory, extracted again
once when Slack answers invalid_auth. It is the only way to
client.counts, the client call that answers every unread at once; the
public API has no unread endpoint. auth = "token" sends a user token
(xoxp-) as a bearer and falls back to conversations.info per
conversation: direct messages and unread channels, no mention counts, no
threads. Without either, one hint row per palette; Enter opens the
extension's settings.
Unreads. Addressed versus merely unread: direct messages, mentions
(@you, @here, @channel) and replies in followed threads are the
rows and the count; channels that are only unread are named last, no
count, their topic or purpose as the subtitle (else "New messages": no
run is fetched for them). Sections Direct messages, Mentions, Threads,
Channels, newest first in each. The row: the conversation's name, the sender's avatar, the
message (in a channel the one naming you, not the last), a count badge
(red; blue for thread replies; 1+ past a page), a presence dot on a
direct message (green active, grey away; users.getPresence per person,
a minute; the presence setting turns it off), the time. The pane is
the unread run, oldest first, up to eight messages. One client.counts
per refresh; conversations.history only for an addressed conversation,
the newest twelve, once per change of its latest; the inbox is shared
with the bar item for 30 s.
| action | shortcut | when |
|---|---|---|
| Open in Slack | Enter |
slack://channel?team=&id=&message= |
| Reply | ⌘Enter |
not a thread row; a one-field form, chat.postMessage (into the thread for a threaded mention) |
| Mark as read | ⌘⇧R |
not a thread row; conversations.mark at the latest message |
| Open in browser | ⌘⇧O |
the archive page |
| Copy link | ⌘C |
Channels. Every channel, private channel, group message and direct
message you are in (users.conversations, an hour), channels first then
by name, with the topic or purpose, the member count and a tag for
private, group, DM. Actions: Open in Slack, Open in browser (⌘⇧O),
Copy link (⌘C).
Search Slack. search.messages with the query as typed, Slack's
syntax through (from:@name, in:#channel, has:link,
before:yesterday), newest first; rows are the message, who said it
where, the time. A keystroke waits 300 ms for the next. A hit shows once
its names are resolved, in Slack's order: while a sender the directory
lacks is asked for, the hits above it are already up. Actions: Open in
Slack (at the message), Open in browser (the permalink), Copy text.
Status. What is set now first (the status with its expiry, Do Not
Disturb, presence; Enter clears, ends, flips), then the statuses
presets (:emoji: text (expiry), common Slack shortcodes drawn as the
emoji), Do Not Disturb for 30 minutes, an hour, until tomorrow, and Set
away / Set active. users.profile.set, dnd.setSnooze / endSnooze,
users.setPresence; every pick keeps the palette open with a toast.
The bar item slack/unreads: the count of what is addressed (DMs +
mentions + thread replies) as the badge, hidden at zero, urgent while a
direct message waits (its dm rule); every refresh seconds and on show,
wake, network. The popover is a view: a section per kind with the newest
rows (avatars with a direct message's presence dot, the latest line, a
count badge; Enter opens one), the
quiet channels as badges, a reply field on r, mark read on m, all
read on a, the palette on p. A failed refresh leaves it stale; not
signed in hides it.
Popover keys of slack/unreads (rendered every 120 s and on show, wake,
network; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
enter |
Open the row in Slack |
up |
Move the cursor (down, j, k too; a click sets it) |
r |
Reply: the search row becomes the field, Enter sends |
m |
Mark the row read |
a |
Mark all read (cmd+shift+a too) |
o |
Open Slack |
p |
Open the Unreads palette |
cmd+shift+o |
Open the row in the browser |
cmd+c |
Copy the row's link |
Settings, [extensions.slack]:
| key | type | default | what |
|---|---|---|---|
auth |
app / token |
app |
The desktop app's session, or a user token. |
token |
secret | (none) | The user token, with auth = "token". |
workspace |
text | (none) | The workspace when the app is signed in to several (id or domain); empty lists every one. |
statuses |
list | five presets | :emoji: text (expiry) per line; expiry 30m, 2h, 1d, today, or none. |
presence |
boolean | true |
A presence dot on each direct message row (green active, grey away): one users.getPresence per person, remembered a minute; off makes no such call. |
unreads item settings, [bar.items."slack/unreads".settings]:
| key | type | default | what |
|---|---|---|---|
refresh |
number (s) | 120 |
Seconds between refreshes of the item, 10 at least. |
For the tests, PAL_SLACK_API replaces the API host, PAL_SLACK_APP_DIR
the app's directory and PAL_SLACK_TOKEN the token setting.
Menu Bar Items (menu-bar)
The menus of the app in front as one list, Raycast's Search Menu Bar Items: every enabled item with the menus above it as the subtitle ("File
Export"), its shortcut as key caps, a check mark when it is on, the app's icon on every row, a section per top menu in menu order. Live, so the menus are read again every time the panel shows (pal's panel never takes the app's place, so the app behind it is the one read) and the rows are root results:
export pdfat the root finds Preview's item without opening the palette; the whole path is searched (the segments are keywords, and so is the app's name).
Enter presses the item: pal hides first and waits a few frames for key
focus to return to the app, so an item that opens a sheet or a dialog
lands there, then presses the very element it listed through Accessibility
(AXPress); the HUD names the app and the path ("TextEdit: File > Export
as PDF…"), or says why the press failed. A pick more than two seconds
after the listing, or of an id it does not have (pal run 'menu-bar/menu-bar/File > New', an item hotkey, the app in front changed
since), reads the menus once more first, so the press lands in the app in
front now and item_hotkeys on a menu item work as long as its path is
stable.
What is left out: separators and disabled items (what the app would not
let you click now), the Apple menu (the System palette has what matters
there), submenus deeper than four levels (File > Export > As > PDF is the
deepest listed) and whatever was not reached within 150 ms of reading. The
walk is one round trip to the app's main thread per item (seven attributes
in one AXUIElementCopyMultipleAttributeValues) plus one per submenu, so a
warm app is fast: Finder 40 items in 18 ms, kitty 54 in 7 ms, TextEdit
with a document 144 in 11 ms, Chrome 271 in 81 ms (deep bookmark folders
cut). An app answers slowly for a moment right after it comes to the
front (TextEdit just activated: 22 items in 188 ms; a second later 144 in
11 ms), so a walk the budget cuts short is answered with the last fuller
listing of the same app when there is one, and pressing checks the item is
still enabled.
- macOS: needs the Accessibility permission (the menus are read through
the app's
AXMenuBar); without it the one row says so andEnteron it opens System Settings on the pane. Fn (Globe) shortcuts have no Accessibility bit: a bare letter with the "no command" flag is drawn asfn F, which is what Enter Full Screen carries. - Linux: not available; no desktop exposes an app's menus to read. The one row says so.
No settings.
Generate (generate)
Identifiers, secrets, random values, hashes, encodings, lorem ipsum, a
random colour, a QR code and a JWT taken apart, from generate/.
An input palette: the rows come from what is typed, and every value
copies on Enter, pastes on ⌘Enter; ⌘R (Refresh) lists again
with fresh values, which is how a value is regenerated.
The empty query lists one fresh value of every generator: UUID v4, UUID
v7 (time-ordered), ULID, Nano ID, a password (password_length
characters from password_charset, the strength as a tag on the right:
bits of entropy, weak under 36, fair under 60, good under 80, strong
under 128), a passphrase (passphrase_words words from a list of 2551
common five-letter words, about 11 bits each), a random number (1 to
100), 16 random bytes as hex and as base64, a lorem ipsum paragraph, a
random colour (the swatch as its icon, ⌘O opens it in the Colour
Picker). A mode word narrows and parameterises: password 32 alnum,
passphrase 7, number 1-6 (or dice), hex 32, bytes 32, lorem 3 paragraphs, colour (five). Anything else filters the generators by
name and keyword.
A transform mode works on the text after it, or on the newest clipboard
text when nothing follows (the subtitle says which): sha256, md5,
sha1, sha512, hash (all four, ⌘⇧C copies them as lines),
base64, b64url, url, hex (each decoding first when the text is
already that), encode (every form), decode (whatever the text turns
out to be: base64, URL escapes, hex, a JWT). qr <text> is one row
wearing the code as its icon, drawn large in the detail pane; Enter
shows it full width, ⌘C copies the SVG. The encoder is the
extension's own (qr.ts, byte mode, versions 1 to 40, after Nayuki's
qrcodegen). jwt <token> is the header, the payload with its expiry as a
tag, a row per time claim (exp, iat, nbf) as a date with the
relative time, and a reminder row: the signature is never verified.
| keys | action |
|---|---|
enter |
Copy the value; show the QR code large on its row |
cmd+enter |
Paste the value into the app in front |
cmd+shift+c |
Copy the whole group: all hashes, all encodings, the JWT's header and payload |
cmd+o |
Open a colour in the Colour Picker |
cmd+c |
Copy the QR code as SVG |
cmd+r |
List again: every value regenerated |
Settings, [extensions.generate]:
| key | type | default | what |
|---|---|---|---|
password_length |
4 to 256 | 20 |
Characters in a password; password 32 overrides it once. |
password_charset |
full, alnum, letters, digits |
full |
What a password is drawn from; with full or alnum one of each class is guaranteed. |
passphrase_words |
2 to 20 | 5 |
Words in a passphrase; passphrase 7 overrides it once. |
passphrase_separator |
text | - |
Between the words. |
Shortcuts (shortcuts)
Every shortcut from the Shortcuts app, from shortcuts/, over
the shortcuts command line tool (macOS 12 and later): indexed and
primary, so a shortcut's name at the root finds it; the folder is the
section inside the palette and a keyword. Enter runs the shortcut and
the panel hides; when the run ends, however long it took, the HUD shows
<name>: Done, the first line of what the shortcut output, or the tool's
message when it failed. ⌘Enter runs it with the newest clipboard
text as its input (a temporary file handed to shortcuts run -i),
⌘T asks for the input in a form, ⌘O opens the shortcut in the
Shortcuts app, ⌘C copies its name. The listing is kept five minutes
(ttl); ⌘R lists again now.
| keys | action |
|---|---|
enter |
Run the shortcut |
cmd+enter |
Run it with the clipboard's text as input |
cmd+t |
Run with input: the input typed in the bar (a form for a pick without it) |
cmd+o |
Open it in Shortcuts |
cmd+c |
Copy its name |
A global hotkey per shortcut is [palettes.shortcuts.item_hotkeys] with
the shortcut's name as the key ("Lights On" = "ctrl+alt+l"); a second
shortcut of the same name has <name> (<identifier>) as its id. The
Shortcuts database is behind macOS's privacy guard and the tool does not
expose a shortcut's icon or colour, so every row wears the palette's
mark. On Linux, or a Mac without the tool, the palette is one row saying
so.
Script Commands (scripts-commands)
Part of the scripts extension: every executable file in the commands
folder (~/.config/pal/commands by default, watched) whose header has a
# @pal.title line is one row. The header says the mode (hud, the
default: the HUD shows the first output line; silent; show: the whole
output as a level; list: the output's JSON lines as rows, a row with
url opening, one with copy copying, any other running the script again
with PAL_PICK; inline: the first output line as the row's subtitle,
refreshed every @pal.refresh), the arguments (@pal.args, a form on
Enter), @pal.confirm, @pal.keyword, @pal.section, @pal.cwd and
@pal.icon (an emoji, a glyph, a hex, a brand colour, an image next to
the script, a url). Raycast's @raycast.* headers are read as aliases,
so a Raycast script command drops in unchanged. The row's id is the file
name, the key for [palettes.scripts-commands.item_hotkeys]. The whole
format is in Scripts and data files; two
examples are under examples/commands/.
| keys | action |
|---|---|
enter |
Run (Open, for a list command; a form first with args) |
cmd+o |
Open the script file |
cmd+c |
Copy output: run it and copy what it printed |
cmd+shift+c |
Copy the file's path |
tela (tela-search, tela-research, tela-pages, tela-spaces, tela-new-page, tela-decks, tela-sheets, tela-comments, tela-backlinks)
One extension, nine palettes and a bar item over a tela
instance, signed in with a personal access token. Everything goes to the
instance's own API: the REST routes for lists, pages, search and writes,
and /api/mcp (what tela's tela-mcp package proxies to) for research.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Search tela | tela-search |
input | opens the page in tela |
| Ask tela | tela-research |
input | asks; on a source, opens it |
| Pages | tela-pages |
indexed, 5 min | opens the page in tela |
| Spaces | tela-spaces |
indexed, 1 h | lists the space's pages |
| New Page | tela-new-page |
indexed, 1 h | the form, the space chosen |
| Decks | tela-decks |
indexed, 1 h | opens the deck in tela |
| Sheets | tela-sheets |
indexed, 1 h | opens the sheet in tela |
| Comments | tela-comments |
live | marks read and opens the page |
| Backlinks | tela-backlinks |
indexed, 5 min | opens the linking page |
Signing in. base_url is the instance, token a personal access
token from Settings, API Keys on tela (tela_pat_..., kept in the
keychain). A read token lists, searches and researches; a page, a
comment and marking read need write scope. Without either
setting every palette is one hint row naming which; a 401 is one naming
the renewal, Enter on tela's API Keys page.
Search tela. Ranked full-text over titles and bodies (a phrase in
quotes, -word excluded), 250 ms after the last keystroke; rows by
space with the breadcrumb and the matching passage, a public-only hit
tagged; the pane is the page. Ask tela. tela's research: a numbered
grounding, the cited sources, disagreements, a low-confidence flag. Enter
on the question opens the answer as a view: the flags on top, the
sources as numbered rows, the selected one's excerpt rendered under its
row. ↓/j, ↑/k, a digit select; Enter opens the source, ⌘Enter
reads it in pal, ⌘C its link, ⌘⇧C the grounding, ⌘⇧O the question
on tela's Ask page, + twice the sources when truncated, n a new
question in the search row. Twelve questions are remembered; ⌘⇧D
forgets one.
Pages. At the root four commands (New tela page, Search tela, Ask
tela, Quick Notes), favourites, then the pages that changed lately by
space with who changed them; from Spaces, one space's tree by top-level
page. On every page row: Open (Enter), Read in pal (⌘Enter, the
markdown drawn as a view: headings, lists, callouts as tinted cards, code
and quotes on sunken wells, tables as aligned columns, links under their
paragraph; bold and code inside a paragraph are flattened, the view's
text is one run), Copy link (⌘C), Outline (⌘⇧O), Backlinks (⌘B),
Comment (⌘⇧M, anchored on a run of the page's text).
Spaces: every space with its page count, tags for public, personal and
the default; ⌘Enter opens it, ⌘N starts a page in it. New Page: a
form whose body starts as the front app's selection, else the clipboard's
text. Decks and
Sheets: the flagged pages across every space, newest first; a deck's
pane leads with its first slide. Comments: mentions and replies to your
comments, unread first; Enter marks read and opens, ⌘⇧R marks read,
⌘⇧A marks all. Backlinks: what links to the page opened last, or to
the row ⌘B came from.
The bar item tela/inbox: unread mentions and replies as the badge,
hidden at zero; every 300 s and on show, wake, network. The popover:
every one as a row (it scrolls), Open in pal, Mark all read.
Not there: a daily page (tela has none; Quick Notes stands in), an append
or any edit of a page's body (that is tela's MCP, not a launcher's job), a
space filter on Search (the REST search takes none), an LLM answer (that
is tela's Ask page, ⌘⇧O).
Settings, [extensions.tela]:
| key | type | default | what |
|---|---|---|---|
base_url |
text | (none) | The instance's origin. |
token |
secret | (none) | A personal access token from Settings, API Keys. |
default_space |
text | (none) | The space New tela page offers first: name, slug or id. |
research |
boolean | true |
Show Ask tela; needs an embedder on the instance. |
For the tests, PAL_TELA_URL and PAL_TELA_TOKEN replace the two settings.
Apple TV (appletv-remote, appletv-now, appletv-play, appletv-apps, appletv-commands, appletv-users, appletv-setup, appletv/playing)
An Apple TV on the same network, spoken to directly over the two protocols
the iPhone's remote uses: Companion for the buttons, swipes, apps, users,
sleep and wake, the volume and the text field, and MRP over AirPlay for what
plays. No hub, no Home Assistant. One connection to the current Apple TV
(the device setting) is opened on first use; everything the TV tells (a
new item, a pause, sleep, a keyboard on screen) pushes into the open remote
and the bar item, so nothing polls. The extension's README
(appletv/README.md) has the protocols, the pairing and every
key.
Set Up Apple TV (appletv-setup, a view) walks Find, Remote, Now
playing and Ready: the Apple TVs on the network with their model and tvOS
version (a digit pairs one, i takes an address, r looks again), then the
four-digit code the TV shows, typed in the search row's field (a wrong one
brings a new code), then a second code for what plays (s skips it), then a
check of what works read from the TV itself. Until one is paired the other
palettes are a single "Set Up Apple TV" row, the remote is a welcome and the
bar item is hidden.
Apple TV Remote (appletv-remote, a view): the clickpad on the arrows
(each part lights when its key is pressed, and clicks), Enter selects
(shift+enter holds), Backspace goes back, h home, tab the app
switcher, c Control Center, space play or pause, , and . skip by
skip seconds, cmd+left/cmd+right previous and next, - and = the
volume, shift and an arrow swipes, p sleep or wake, s the screen
saver, u users, d the next paired Apple TV. The card shows what plays
with its cover and a seek bar a click moves; the dock (favorites, else
the apps opened most) puts apps on the digits; the volume is a slider when
the TV set reports it over HDMI-CEC. When the TV shows a keyboard a banner
says what for, and t types into it from the search row.
Now Playing on Apple TV (appletv-now, a view; n on the remote)
sits what plays on its cover's colour: the cover, the title, the show with
its season and episode (or the artist and album), the facts and a line of
the synopsis, a position bar split at the chapters, the transport as large
buttons with speed, subtitles, audio and like beside them. Left and right
scrub (each press moves the target further, faster while held; one seek
when the keys rest), Tab cycles a panel: Up Next with pictures, Chapters,
Audio, Subtitles, Lyrics, Info.
Play on Apple TV (appletv-play, input, inline at the root on a
playable url) sends a link to the TV: a YouTube video at its start time, a
Netflix title or an Apple TV+ page. It lists the link just copied, the browser tab in front, the
clipboard history's links and what was played before. The tab in front
says whether it is playing or paused there, and a YouTube video it is
partway through starts on the TV where the tab is (the DevTools probe's
position; cmd+enter from the start), leading the list so the same video
copied does not start over. Whatever starts on the TV, from any of these,
pauses a browser tab playing the same video. A newly copied link
is offered by the bar item (90 s), the remote's banner (l) and the empty
root's Now section (10 min).
Apple TV Apps (appletv-apps) lists every app with its App Store icon
(Apple's own drawn from their marks); Enter opens it on the TV, cmd+enter
opens it and shows the remote, cmd+d adds it to the dock or takes it out.
Apple TV (appletv-commands, primary) has play or pause, wake, sleep,
home, Control Center, the screen saver, skip, next, previous, the volume,
Type on Apple TV (Tab, then the text), switch user, switch Apple TV and set
up at the root; the empty root's Now section has what plays. Apple TV
Users (appletv-users) switches the signed-in user.
The bar item (appletv/playing): the app's icon and what plays, muted
while paused, hidden while nothing plays or the TV sleeps (the when
setting keeps it while the TV is awake); the popover is the remote. It
publishes appletv/power, appletv/playing, appletv/app and
appletv/title.
| Setting | Default | |
|---|---|---|
device |
"" |
The current Apple TV by name; empty: the one paired last |
favorites |
[] |
The dock's apps by name or bundle id |
skip |
10 |
Seconds a skip moves |
wake |
true |
Wake the Apple TV when the remote opens |
stay |
true |
Keep the connection open in the background for the bar item |
Hue (hue-rooms, hue-lights, hue-scenes, hue-light, hue-setup, hue-sensors, hue-automations, hue-entertainment, hue/home)
Philips Hue over the bridge's CLIP v2 API on the LAN (https, the
hue-application-key header), nothing through the Hue cloud beyond one
discovery lookup during setup. One GET /clip/v2/resource per bridge reads
the home whole; the bridge's event stream (/eventstream/clip/v2) patches
it from then on, so every palette below is drawn from memory and follows a
change made in the Hue app, on a switch or by an automation without a
request. A change from pal is one PUT, applied to the model at once and
confirmed by the stream; per resource the newest state goes out no more
often than Hue asks (10 a second to a light, 1 a second to a group), so a
held arrow key is one command. Several bridges make one home. The
extension's README (hue/README.md) has the pairing story, the
TLS pinning (Signify's root-bridge CA plus the certificate pinned at
pairing; insecure skips it) and every key.
Set up Hue (hue-setup, a view) finds the bridges (the cloud endpoint
and mDNS; r scans again, i takes an address typed), asks each its
name and id with no key, and 1..9 (or Enter) starts press-link: "Press
the round button on the bridge" with a thirty-second countdown while a
background task asks the bridge every second; x stops a pairing (or
forgets a bridge), b goes back to the bridges, Escape leaves and the
panel comes back on its own once the key is in. The key lands in the
keychain and the address in the config file through settings.set
(Settings › Extensions › Hue shows both); c copies the key once paired.
Until a bridge is paired every other palette is one "Set up Hue" row and
the bar item is hidden.
Hue Rooms (hue-rooms, live, primary) lists rooms then zones with a
tile of the lit lights' colours as stripes (faded by the brightness, an
outline when off), the count on and the grouped brightness; Enter toggles
the grouped light, ⌘Enter opens the room under the keys, ⌘S its
scenes, ⌘L its lights, ⌘⇧C copies the id. Hue Lights (hue-lights,
live) is every light under its room's section with its colour as a
swatch, the archetype, the kelvin, unreachable, the effect and the
brightness; Enter toggles, ⌘Enter opens, ⌘B blinks, ⌘C copies the
hex. Hue Scenes (hue-scenes, live, primary) is every scene by room as
a five-swatch strip of its palette, tagged active, playing, dynamic
or smart; Enter recalls it with the transition, ⌘Enter plays the
palette dynamically, ⌘O opens the room. Rooms and scenes are primary at
the root: living room and Enter toggles the room, relax and Enter
plays the scene.
Hue Light (hue-light, a view, opened from a row) puts a light or a room
under the keys: the tile in its colour with the brightness, the brightness bar,
the temperature on a warm-to-cool strip with a marker across the light's mirek
range, the hue/saturation plane with a marker (the same gradient node as the
colour picker), then the presets (Relax, Read, Concentrate, Energize, Bright,
Dimmed, Nightlight), the room's scenes as strips, the effects the light supports
and the options. ←/→ brightness (5 %, ⇧ 20 %), ↑/↓ cooler/warmer (20
mirek, ⇧ 80), 1..9 and 0, t/space toggle, ⇥ (⇧⇥ back) walks
light, colour (the arrows become hue and saturation, clamped into the gamut),
presets, scenes, effects (←/→ choose, Enter applies), a this light or the
whole room, d the transition (instant, 400 ms, 1 s, 4 s), s scenes, o the
room, i blink, c copy, r re-read. The view follows the bridge: a change
from a switch or the Hue app reaches it through the event stream while it is
open (the tree pushed, view.update), no key needed.
Hue Sensors (hue-sensors, live): motion, temperature, light level
(lux), buttons and dials with their last event, contact sensors, by
device with the battery on the device's first row (red when low) and when
the reading changed; Enter copies the reading, ⌘E enables or disables.
Hue Automations (hue-automations, live): the behaviour instances
with their script and status; Enter enables or disables. Hue
Entertainment (hue-entertainment, live): the areas; Enter starts
streaming, ⌘Enter stops.
The bar item (hue/home): the main room's colour as a dot (a PNG; the
item's main_room setting, else the room with most lights on) and N on; the
popover toggles every room, plays the scenes (the item's scenes, else the main
room's), opens pal, turns everything off. Rendered every 60 s and on show,
wake and network, pushed on every stream event (at most every 300 ms).
Popover keys of hue/home (rendered every 60 s and on show, wake, network; the
arrows move the cursor, a click sets it):
| keys | does |
|---|---|
up |
Move over the rooms; walk an opened room's lights |
enter |
Open the room under the cursor; toggle the light under it |
space |
Toggle the room (or the light) under the cursor |
right |
Brighter by 5 (shift: 20); left dims |
backspace |
Close the opened room |
1 |
Play the scene with that digit (1 to 9) |
e |
Everything on |
x |
All off |
p |
Open Rooms in pal |
r |
Read the bridge again |
Links: pal://hue/toggle?room=living-room (on=1 sets),
pal://hue/scene?name=relax&room=living-room (dynamic=1),
pal://hue/off. Ids are slugs (room:living-room,
scene:living-room/relax, light:sofa-lamp), so
[palettes.hue-rooms.item_hotkeys] with "room:living-room" = "ctrl+alt+l" toggles the room without showing pal.
Settings, [extensions.hue]:
| key | type | default | what |
|---|---|---|---|
bridge |
text | unset | The bridge's address, for a key kept in the keychain; pairing needs neither. |
application_key |
secret | unset | The key for bridge, a keychain: or env: reference. |
insecure |
boolean | false |
Skip the certificate check (a bridge behind a proxy). |
transition |
number (ms) | 400 |
How long a change from a row or a link takes; the view cycles its own with d. |
timeout |
number (s) | 5 |
One request's limit. |
home item settings, [bar.items."hue/home".settings]:
| key | type | default | what |
|---|---|---|---|
main_room |
text | unset | The bar dot's room and the popover's scenes. |
scenes |
list | [] |
Scene names or ids in the popover, in order. |
Spotify (spotify-now-playing, spotify-search, spotify-playlists, spotify-library, spotify-devices, spotify-queue, spotify-commands, spotify/playing)
One extension over the Spotify Web API, signed in to your own Spotify
app with PKCE (no secret; spotify/README.md says how to
create the app and register http://127.0.0.1:27182/callback). The
refresh token lives in the extension's storage file, since the SDK has
no settings.set to write a keychain: reference through; Sign Out
deletes it.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Lyrics | spotify-now-playing |
view | play or pause; the transport on keys |
| Search Spotify | spotify-search |
input | plays the row |
| Playlists | spotify-playlists |
indexed, 5 min | plays the playlist; cmd+enter lists its tracks |
| Library | spotify-library |
indexed, 5 min, filters | plays the track |
| Spotify Devices | spotify-devices |
live | transfers playback there |
| Queue | spotify-queue |
live | skips to the row |
| Spotify | spotify-commands |
indexed, primary | play or pause, next, previous, like, lyrics, sign out, "Play <pinned playlist>" |
Search lists Tracks, Artists, Albums, Playlists, Podcasts and
Episodes as sections with the cover as the icon. A track: Enter plays,
⌘Enter queues, ⌘L likes or unlikes, ⌘O opens in Spotify,
⌘C copies the link; a playlist or album: Enter plays it as the
context, ⌘Enter its tracks as a level (a row there plays from that
point inside it), ⌘S shuffled. Library's filters (Tab): Liked Songs
(newest first), Recently played, Top tracks, Top artists (the last
weeks). Devices: a row per device with its glyph, volume and
active tag (Enter plays there, ⌘Enter transfers without playing),
plus Volume up, down and Mute rows for the active one (⌘↑/⌘↓ from
any row).
Queue: what plays now, then the queue numbered; the Web API cannot
remove a queued track, so Enter skips to the row (one Next per row).
The Spotify rows are root results (pause, next, like, Play Focus); the playing track is also a Now row at the empty root.
The lyrics view (spotify-now-playing): the cover, the track as the headline, a
ticking progress bar, badges (paused, shuffle, repeat, liked, the device
and its volume) and lrclib.net's synced lyrics around the line playing,
the line bright at the largest size, the three before muted, the three
after faint, sliding up as the song goes; unsynced lyrics scroll with
the position; "No lyrics on lrclib" with f to search there. The
cover's dominant colour is the progress bar's colour. Keys: space play or pause, left/right seek 10 s,
up/down volume, l like, s shuffle, r repeat, q queue, d
devices, ⌘→/⌘← skip, ⌘C copy the line, ⌘O
open in Spotify. Every key answers with the next tree at once from a
locally patched state, and the view follows the song while it is open:
the tree is pushed every second while something plays (the lines slide
up on time), with a re-ask every 5 s as the safety net.
The bar item (spotify/playing): the track, or with its lyrics
setting the lyric line playing; hidden while nothing plays. The popover is the
lyrics view in a compact layout (the cover top-left, a slim progress
bar, the line playing with one before and two after, the transport and
the like/device/queue row as keycap hints), pushed every second while
the popover is open (the shell says when it opens and closes; five
minutes after the last action as the fallback; Spotify read every 5 s,
the clock between reads), and outside that window the item asks to be
rendered again when the next line starts. Rendered every 30 s, on show,
wake, network and the media trigger.
Popover keys of spotify/playing (rendered every 30 s and on show, wake,
network, media; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
space |
Pause or play |
cmd+right |
Next track |
cmd+left |
Previous track |
right |
Seek 10 s forward |
left |
Seek 10 s back |
up |
Volume up |
down |
Volume down |
l |
Like or unlike |
s |
Shuffle on or off |
r |
Repeat all, one, off |
q |
Open the queue |
d |
Open the devices |
cmd+c |
Copy the current line (or the track) |
cmd+o |
Open in Spotify |
Settings, [extensions.spotify]: client_id (text), redirect_port
(number, 27182), pinned (list of playlist names or
spotify:playlist: links). playing item settings,
[bar.items."spotify/playing".settings]: lyrics (boolean, true, the
lyric line on the strip instead of the track name). A 429 is waited out
under two seconds and otherwise refused locally until its Retry-After;
offline, no device and Premium-required states are one line each.
Obsidian (obsidian-notes, obsidian-search, obsidian-daily, obsidian-tags, obsidian-recent, obsidian-backlinks, obsidian-outgoing)
One extension, seven palettes over an Obsidian vault on disk: the folder
in the vault setting, else the one Obsidian has open in its own
obsidian.json. Nothing goes through Obsidian itself except opening a
note (obsidian://open); the notes are read as files.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Notes | obsidian-notes |
indexed, 5 min, primary | opens the note |
| Search Notes | obsidian-search |
input | opens the note |
| Daily Notes | obsidian-daily |
live | opens today's note, or creates it |
| Tags | obsidian-tags |
indexed, 5 min, catalog | lists the notes with the tag |
| Recent Notes | obsidian-recent |
live | opens the note |
| Backlinks | obsidian-backlinks |
indexed, 5 min | opens the linking note |
| Outgoing Links | obsidian-outgoing |
indexed, 5 min | opens the linked note |
Notes. Every .md in the vault (dot folders and the exclude globs
out) as a row: the title (front matter title, else the first #
heading, else the file name), the description (front matter, else the
first body line) as the subtitle, the folder as the section, a tag (not
the folder's) and the change date on the right; the file name, the aliases, the
tags and the folder are keywords. Four commands lead: Today's note
(⌘Enter appends to it), New note, Search notes (⌘Enter opens
Obsidian's search), Random note. On a note: Open in Obsidian and Open in
editor (Enter and ⌘Enter, swapped by the open_with setting; the
editor command gets the path, empty is the OS opener), Copy wikilink
(⌘C: [[name]], [[folder/name]] when the name is shared), Read in
pal (⌘⇧R: the note as a view through tela's markdown renderer, with
callouts, tables, tasks, code), Backlinks (⌘B), Outgoing links (⌘L),
Copy path (⌘⇧C). The pane (⌘I) leads with the vault and folder over
the title, the tags as chips and the words, backlinks and links large,
then the note as markdown (front matter off, callouts a bold lead,
wikilinks as links into Obsidian) over its modified time, aliases, links
and the notes linking to it. The index
is per file by mtime and size; a watcher on the vault marks it stale, the
next listing rebuilds it.
Search Notes. Full-text as you type, the words as typed, case-
insensitive: ripgrep over the vault when rg is on PATH, else a scan in
Bun cut at one second. Rows by folder with the first matching line as the
subtitle and the count of matching lines; the pane lists them with the
match bold; a note named like the query first. Daily Notes. Today's
note (Create today's note when missing, asking first, from the template),
yesterday's, the last seven days under This week; then Append to today (a
form prefilled from the clipboard; {selection}, {clipboard},
{date}, {time} filled; the text lands as the last line, the note
created when missing) and New note (title, a folder of the vault, the
body from the template setting with {{title}} and {{date}} filled,
else your selection or the clipboard; a title already taken is refused).
The folder, name format and template of a daily note come from the
daily_* settings, else Obsidian's .obsidian/daily-notes.json, else
YYYY-MM-DD; the format is moment's tokens as Obsidian takes them.
Tags: every tag with its count, most used first; Enter lists the
carriers (a nested #a/b counts under #a too). Recent Notes: the
twenty changed last. Backlinks and Outgoing Links: for the note
opened last, or the row ⌘B / ⌘L came from; a link to nothing yet
offers Create the note.
Links: pal://obsidian/open?path=, pal://obsidian/new?title=&body=&folder=,
pal://obsidian/append-today?text=.
Settings, [extensions.obsidian]:
| key | type | default | what |
|---|---|---|---|
vault |
path | (none) | The vault folder; empty takes the vault Obsidian has open. |
open_with |
select | obsidian |
What Enter does on a note (obsidian or editor); ⌘Enter is the other. |
editor |
text | (none) | The command Open in editor runs with the path last; empty opens with the OS. |
daily_folder |
text | (none) | Where daily notes live; empty reads the plugin. |
daily_format |
text | (none) | The daily note's name; empty reads the plugin, then YYYY-MM-DD. |
daily_template |
text | (none) | The template a new daily note starts as; empty reads the plugin. |
template |
text | (none) | The template New note starts as. |
exclude |
list | [] |
Globs relative to the vault left out of every palette. |
Not there: editing a note's body, Obsidian's search operators (tag:,
path:), weekly and monthly notes, canvases and attachments.
Gmail (gmail-inbox, gmail-search, gmail-labels, gmail-compose, gmail-drafts, gmail/unread)
One extension, five palettes and a bar item over the Gmail API, multi:
one instance per account ([extensions.gmail] and
[extensions."gmail@work"] next to [instances."gmail@work"]), each
with its own token command, address, palettes, bar item and storage; the
titles read "Inbox (Work)", the tile takes the instance's tint and badge.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Inbox | gmail-inbox |
live, lazy | opens the thread in the account's Gmail |
| Search Mail | gmail-search |
input | opens the thread |
| Labels | gmail-labels |
indexed, 1 h, catalog | opens the label in Gmail |
| Compose | gmail-compose |
list (one row, send on) | the form, then sends |
| Drafts | gmail-drafts |
live, lazy (send on) | opens the draft in Gmail |
The token. token_command (per instance) is a shell command whose
stdout is an access token, bare or as an OAuth endpoint's JSON
(access_token, expires_in): the calendar extension's pattern. Kept
in memory until its expiry (30 min for a bare one), minted again once on
a 401, never written. gcloud auth application-default print-access-token after a login with gmail.modify; the owner's two
identities through his broker (aud=gmail, aud=gmail-work). A failing
command is one hint row with its last stderr line and Open Gmail
settings; an empty one names the fix; a rejected token says to check the
scopes. The bar item hides without a token, goes stale on anything else.
Inbox. messages.list with maxResults 50 for the inbox's unread,
then the inbox, then each labels entry's unread; messages.get with
format=metadata (From, To, Cc, Subject, Date, Message-ID, Reply-To,
References) eight at a time, cached by id for the process, the labels
patched on every write. Sections Unread, Recent, then one per labels
entry. The row: the sender's Gravatar (a HEAD per address, remembered)
or the initial on a tile tinted from the address, the subject, the
sender and the snippet, the user labels as chips (two), a star tag, a
paperclip when the top-level MIME type is multipart/mixed (a guess:
metadata sends no parts; the pane confirms), the date. The pane
(format=full, the last twenty kept): the text body with the quoted
replies folded (On ... wrote: and > runs; Gmail's gmail_quote,
blockquote, Outlook's divRplyFwdMsg in HTML), else the HTML as text
(lists as a - bullet, links as text (url)), markdown-escaped so a <a@b>
survives; From, To, Cc, Date, Labels, Attachments (name and size),
Open in Gmail. The unread count is the list's length under a page,
labels.get INBOX past it. The inbox is shared with the bar item for
30 s.
| action | shortcut | when |
|---|---|---|
| Open in Gmail | Enter |
https://mail.google.com/mail/u/<address>/#inbox/<threadId>; #all/ off the inbox |
| Mark as read / Mark as unread | ⌘Enter |
messages.batchModify over the row or the marked rows; the one that flips the row is on ⌘Enter, the other at the end, so marked read and unread rows offer both |
| Archive | ⌘E |
send on; INBOX removed; marked rows too |
| Star / Unstar | ⌘S |
send on; marked rows too (all starred or all unstarred) |
| Reply | ⌘⇧R |
send on; a form (to, cc, subject, body), the original quoted under the answer and the signature between, messages.send in the thread with In-Reply-To |
| Copy link | ⌘C |
The bare e and s the design asked for would type into the search
box in a list palette, so they are ⌘E and ⌘S.
Search Mail. messages.list with the query as typed (from:,
subject:, has:attachment, newer_than:7d, label:), 50 at most, a
keystroke waiting 300 ms for the next; the rows as Inbox's, sectioned by
the first user label, else Inbox / Sent / Drafts / Spam / Trash /
Archive. The hits show as their headers arrive (a screenful, then all),
each with its initial, and the Gravatars join when the probes answer.
Labels. labels.list once an hour, persisted in storage so a restart
lists with no call; yours by name, then Inbox, Starred, Important, Sent,
Drafts, Spam, Trash and the categories. Enter opens #label/<name> (or
Gmail's own anchor); ⌘Enter pushes Search Mail with label:<name>
typed; ⌘C copies the name.
send, per instance, off by default. Off is read and mark-read
only: no compose, no reply, no drafts, no archive, no star; Compose and
Drafts list nothing and the row keeps Open, Mark as read, Copy link.
That is the rule for the owner's work account (the token can send, the
extension does not; README). On: Compose is one row opening a form (to,
cc, subject, body with the signature under it) that sends
messages.send; Drafts lists drafts.list with the recipient and the
subject, ⌘Enter sends after a confirmation (drafts.send), ⌘D
discards after one.
The bar item gmail/unread: the inbox's unread count as the badge,
hidden at zero, the instance's title as the strip text when it has one
("Personal", "Work"), the address in the tooltip; every 120 s and on
show, wake, network. The popover is a view: every unread as a row (it
scrolls: the sender's mark, who wrote it, the subject and its snippet, the
time, a star or a paperclip), a cursor the arrows move and a click sets;
Enter opens the message in Gmail, m marks it read, s stars it, a
marks every listed message read, o opens Gmail, p the Inbox palette.
Rows mark as a list's do (⌘-click, ⇧-click, ⇧↓), and m then marks
them all read at once.
Settings, [extensions.gmail] (and [extensions."gmail@work"]):
| key | type | default | what |
|---|---|---|---|
token_command |
text, scope: instance |
(none) | The command that prints the access token. |
address |
text, scope: instance |
(none) | For the links and the tooltip; the profile's when empty. |
labels |
list | [] |
Labels whose unread mail Inbox lists besides the inbox, by name. |
send |
boolean, scope: instance |
false |
Compose, reply, drafts, archive, star. |
signature |
text | (none) | Under a composed or replied body. |
A 429 (or a 403 naming the quota) is remembered for Retry-After (60 s
without one) and every call until then refused locally, one hint row.
For the tests, PAL_GMAIL_API replaces the API host and
PAL_GMAIL_AVATARS the Gravatar host.
Translate (translate, translate-history)
Text translated as it is typed, from translate/. An input
palette: 350 ms after the last key the text goes to the backend and the
rows come back: the translation first (Enter copies, ⌘Enter
pastes, ⌘⇧S speaks it aloud, ⌘⇧C copies the source,
⌘O opens the pair in the Google Translate web app), then the
translation in Latin letters when its script is not Latin, the detected
language with the detector's confidence, the alternatives Google offers,
the source romanised, dictionary entries for a word, and a Swap row that
translates the result back the other way (a push with the pair
reversed). The detail pane holds both texts, the pair, the backend and
the confidence.
The target is the to setting, else the system language; the source is detected
unless from names one. A prefix names the ends once, at the root too
(inline): tr: hello, >de hello, german: hello (the target), en>tr merhaba, turkish>english merhaba (both); a word only counts as a language
when it is one (todo: buy milk is text), and >de takes no space (> and a
space is the Shell palette's root prefix). Nothing typed: the selection in the
app in front, else the newest clipboard text, the subtitle saying which; tr:
alone does the same to Turkish. A text already in the target goes the other way:
to from when it names a language, else to English, else to the system
language; with nothing else to go to (an English text on an English system) the
row says so.
Backends: Google's web endpoint (translate.googleapis.com/translate_a/single,
client=dict-chrome-ex), no key and unofficial (it may refuse a network
it takes for a bot, as gtx did from one home network, or change; the
refusal is one row naming the fix), 5000 characters per request; or
DeepL's v2 API on the free host with api_key (the translation and the
detected source only). Speech is say with a voice of the language on
macOS (Yelda for Turkish), spd-say or espeak on Linux.
Translation History (translate-history, live, its rows at the root):
what was copied, pasted or spoken, newest first, once each, the last
hundred in storage; Enter copies again, ⌘T translates the entry
afresh with the detected source pinned, ⌘D removes it, the last row
clears the history after a confirm card.
| keys | action |
|---|---|
enter |
Copy the translation; on Swap, translate it back |
cmd+enter |
Paste the translation into the app in front |
cmd+shift+s |
Speak the row aloud, in its language |
cmd+shift+c |
Copy the source text |
cmd+o |
Open in Google Translate |
cmd+t |
History: translate the entry again |
cmd+d |
History: remove the entry |
Settings, [extensions.translate]:
| key | type | default | what |
|---|---|---|---|
from |
code or name | auto |
The source language; a prefix overrides it once. |
to |
code or name | empty (the system language) | The target; text already in it goes the other way. |
backend |
google, deepl |
google |
Which translator. |
api_key |
secret | empty | The DeepL key (a free one ends in :fx). |
speak |
boolean | false |
Also read the translation aloud when enter copies it. |
For the tests, PAL_TRANSLATE_GOOGLE and PAL_TRANSLATE_DEEPL replace
the hosts and PAL_TRANSLATE_SAY the speaker.
Shell (shell, shell-history)
One command run in the login shell, its output read in the panel, from
shell/. An input palette: the typed command is one row,
Run: <command>, and nothing runs until Enter; $ ls or > git status at the root lists the same row inline. ⌘Enter on the row
opens the command in a terminal window instead, ⌘C copies it. A
command that looks destructive (rm, sudo, mv, dd, git reset --hard, kill -9, a package manager's uninstall, a redirect onto a disk
device, and so on, judged on the command word of every simple command
of the line) asks first while confirm is on.
The command runs through the shell setting ($SHELL -lic: the login
shell with the profile and the interactive rc, so PATH, aliases and
functions apply) in cwd, with the env lines set, for timeout
seconds (10) at most, then SIGTERM and SIGKILL to the process group.
The answer is a view level: the command as the title, the folder under
it, exit 0 (green) or exit N (red) and the duration as badges,
killed after N s on a timeout and output cut past 256 KB per stream,
stdout in monospace on a sunken surface with stderr under it in red,
scrolling past the panel. A command still running 2.5 s after Enter
gets the view with a running badge and the elapsed time, and the
result lands in place through view.update when it ends, so a command
may outlast the panel's own wait for a pick. In the view Enter copies
the output (stderr when there was none), ⌘Enter opens a terminal on
the command, ⌘R runs it again (the view's own action: a view level
takes the keys the shell's Refresh would), ⌘C copies the command,
⌘⇧E copies stderr.
The terminal is the terminal setting: Terminal (default) and iTerm over
AppleScript, kitty, Alacritty, WezTerm and Ghostty by their flags through
open -na, any other name with -e; on Linux $TERMINAL, else
x-terminal-emulator. The window opens on cd <cwd> && <command>; exec <shell>, so it stays up with the output.
Shell History (shell-history): every command that ran, newest first,
once each, the last hundred in storage, with its exit code as a tag
(killed for a timeout), the duration and the folder; Enter runs it
again (the same confirm), ⌘Enter opens it in a terminal, ⌘C
copies it, ⌘D removes the entry, the last row clears the history.
It is an input palette rather than live: listed on every keystroke so a
command just run is there, and never at the root, where an Enter would
run one.
| keys | action |
|---|---|
enter |
Run the command; in the view, copy the output |
cmd+enter |
Run in a terminal; in the view, open one |
cmd+c |
Copy the command |
cmd+r |
In the view: run again |
cmd+shift+e |
In the view: copy stderr |
cmd+d |
History: remove the entry |
Settings, [extensions.shell]:
| key | type | default | what |
|---|---|---|---|
shell |
text | $SHELL -lic |
The shell and its flags; the command is the last argument. |
cwd |
folder | ~ |
Where commands run. |
timeout |
1 to 600 s | 10 |
The kill after. |
env |
list of KEY=VALUE |
empty | Set for every command. |
confirm |
boolean | true |
Ask before a destructive-looking command. |
terminal |
text | empty (Terminal) |
What cmd+enter opens. |
Not done: stdin (closed), colour (escapes shown as printed), streaming
(the view fills at the end), a session (cd does not stick).
Downloads (downloads)
The Downloads folder newest first, from downloads/. A live,
primary palette: the rows are at the root by name and the folder is read
again on every show. A file still coming in (.crdownload, .part,
.download, .partial) leads under Downloading with the name it will
have, a blue tag, its size and, once it has grown between two listings,
the rate (Safari's .download bundle gives its percentage and total from
its plist); the rest sit under Today, Yesterday, This week and Older by
modification day with the size and the age on the right (the kind is the
icon). An image or a PDF wears a 64 px thumbnail (sips on macOS,
ImageMagick on Linux, made once into the cache directory, the newest 24
per listing); other rows a kind glyph. With browser_folders the
browsers' own download folders (Chrome-family Preferences, Firefox
prefs.js) are listed too when they differ from folder. The last rows
clear what is older than clear_days (the count and size in the row, a
confirm card) and open the folder. The detail pane leads with the folder
over the name, the kind and the size, and adds the url and the page the
file came from (Spotlight's kMDItemWhereFroms). The root's
Now section shows the newest download of the last ten minutes
(suggest).
| keys | action |
|---|---|
enter |
Open (marked rows: every one) |
cmd+enter |
Reveal in Finder / the file manager |
cmd+y |
Quick Look (macOS) |
cmd+c |
Copy the file itself |
cmd+shift+c |
Copy the path |
cmd+m |
Move to a folder (a form; ~ expanded, the folder created) |
cmd+shift+r |
Rename (a form) |
cmd+d |
Move to Trash (asks first; marked rows together) |
tab, shift+↓, cmd+click |
Mark rows |
Open, reveal, both copies and the trash take marked rows (multi). Trash
is Finder's delete on macOS, gio trash on Linux; for the tests,
PAL_DOWNLOADS_TRASH names a stand-in and PAL_DOWNLOADS_CACHE the
thumbnail directory. The rename and move forms are the SDK's
files (sdk/src/files.ts), shared with Files.
Settings, [extensions.downloads]:
| key | type | default | what |
|---|---|---|---|
folder |
folder | ~/Downloads |
The downloads folder. |
browser_folders |
boolean | true |
Also the browsers' own download folders when they differ. |
limit |
10 to 2000 | 200 |
At most this many files, the newest. |
thumbnails |
boolean | true |
Previews for images and PDFs. |
clear_days |
1 to 365 | 30 |
The age the Clear row trashes. |
Google Search (google)
Google from the panel, from google/. An input palette: every
keystroke asks Google's suggest (the homepage's gws-wiz endpoint, which
names the people, places and things it recognises; the documented
suggestqueries one when that fails, decoded from the charset it names,
ISO-8859-9 for Turkish), about 80 ms on a warm connection; a newer
keystroke cancels the older request and answers are kept ten minutes. The
first row is what was typed, then up to eight suggestions; an entity has
its line ("Şarkıcı-şarkı yazarı") and thumbnail, and its Wikipedia card in
the pane (the language's Wikipedia, else English). Tab puts the
suggestion under the cursor in the box (Item.complete). Nothing typed
lists the recent searches.
Web results need a provider (provider): SerpApi (Google's own results,
the answer box and the knowledge panel), the Brave Search API, or a
SearXNG instance with json among its formats. No keyless service gives
results reliably without a browser (tried 2026-09-26: DuckDuckGo's HTML and
lite pages answer a 202 challenge, Bing's RSS feed ranks by the IP's region
and returned Toyota for "react useEffect cleanup", Qwant and Ecosia 403,
Mojeek a captcha, Startpage a proof-of-work page, Google's own page needs
JavaScript). With results = "typing" the suggestions show first
(ctx.partial), and once the query has rested 350 ms the answer and the
top five results join below them, so a query typed straight through
spends one search; the pane previews a suggestion's results 250 ms after
the cursor rests on it. cmd+Enter lists them all as rows (the answer
first, then each result with its favicon, snippet and site), and what is
typed in that level narrows them. The list, the preview and the level
share one request per query.
At the root, when nothing matched, the first three suggestions join the
"Use “q” with" section under Search the web (lateFallback): the page asks
for them only once that section shows, so the section paints at once and
a query the index answered is never sent. Enter searches, cmd+Enter opens
the palette with the suggestion typed, Tab completes it into the root's
box. google (or goo ) jumps into the palette; [palettes.google] alias = "g" makes it g .
| keys | action |
|---|---|
enter |
Search Google in the browser (browser); on a result, open it; on the answer, its source |
tab |
Put the suggestion in the box |
cmd+enter |
Results here (with a provider); on a result, open it in the background |
cmd+b |
Search in the background |
cmd+c |
Copy the text; on a result, the link; on the answer, the answer |
cmd+l |
Copy the search link |
cmd+shift+c |
On a result: copy it as a Markdown link |
ctrl+x |
Remove a recent search (Clear recent searches is in cmd+k) |
Settings, [extensions.google]:
| key | type | default | what |
|---|---|---|---|
provider |
none, serpapi, brave, searxng |
none |
Where results come from; suggestions need nothing. |
serpapi_key |
secret | empty | SerpApi key. The free plan is 250 searches a month: pair it with results = "ask". |
brave_key |
secret | empty | Brave Search API key. |
searxng_url |
text | empty | The instance's address. |
results |
typing, ask |
typing |
Results under the suggestions as you type, or only on cmd+Enter. |
language |
auto or a code |
auto |
Suggestions' and results' language; auto is the system's. Set, it goes into the search url too. |
region |
text | empty | Country code; empty is the system's. |
safe_search |
bool | false |
safe=active, Brave's strict, SearXNG's 2. |
browser |
default or an app name |
default |
Where searches and results open (open -a on macOS, the command on Linux). |
root |
bool | true |
The suggestions under Search the web at the root. |
history |
bool | true |
Remember searches for the empty palette. |
For the tests, PAL_GOOGLE_BASE routes every https://host/path to
<base>/host/path, PAL_GOOGLE_PREVIEW_MS shortens the pane's wait and
PAL_OPEN_URL stands in for the browser (the SDK's openUrl).
GIFs (gifs, gifs-favourites)
Giphy searched from the panel, from gifs/. An input grid:
what is typed is searched 300 ms after the last key, nothing typed lists
what is trending under a Trending section, and every tile is the GIF's
small animated preview (Giphy's fixed_height_small), fetched once into
the cache directory
(~/Library/Caches/pal/gifs, $XDG_CACHE_HOME/pal/gifs) and sent as a
data url. Enter downloads the GIF into the cache, named after its
title, and puts the file on the clipboard (copy_files), so it
pastes as a picture; ⌘Enter copies the url; ⌘O opens the page;
⌘S writes the file to save_to; ⌘F keeps it. The detail pane
(⌘I) shows the preview larger, the size in pixels and bytes, the
page. A root query nothing matched offers "Search GIFs for …".
Favourite GIFs (gifs-favourites, a live grid, its rows at the root):
what ⌘F kept, newest first, the last 200 in storage, with the same
actions and ⌘D to remove; the last row clears it after a confirm
card.
The key is a Giphy app key from developers.giphy.com (Create an App,
API); without one the grid is one row saying which setting to fill and
where the key comes from. content_filter is Giphy's rating: off
(R), low (PG-13), medium (PG), high (G).
| keys | action |
|---|---|
enter |
Copy the GIF file |
cmd+enter |
Copy the url |
cmd+o |
Open the page on giphy.com |
cmd+s |
Save to Downloads (save_to) |
cmd+f |
Add to favourites |
cmd+d |
Favourites: remove |
Settings, [extensions.gifs]:
| key | type | default | what |
|---|---|---|---|
giphy_api_key |
secret | empty | The Giphy app key. |
content_filter |
off, low, medium, high |
medium |
What the results may show. |
save_to |
folder | ~/Downloads |
Where cmd+s writes the file. |
[palettes.gifs.settings] columns (3 to 10, default 6) is the tiles per
row of both grids. For the tests, PAL_GIFS_GIPHY replaces the host and
PAL_GIFS_CACHE the cache directory.
Maps (maps)
Places and directions in Google Maps or Apple Maps, from
maps/. An input palette over urls, no key: a typed place
gets Search, Directions from here, from home and from work, and the
saved places whose name or address contains it; home > work,
here -> Kadıköy or Moda to Levent is a route with both ends and its
reverse (> and -> always split; to only when an end is home, work,
here or a saved place, so "things to do in Moda" stays a search); nothing
typed lists Home, Work, the commute both ways and every saved place. The
travel mode is the filter (driving, transit, walking, cycling; Tab cycles
it). go: coffee or maps: home > work at the root answers inline.
With api_key (a Google Cloud key with Places API (New) enabled), the
rows show at once and place predictions join below them 250 ms after the
last key (ctx.partial), opening pinned to
their place id; the inline ask never waits on them. Google bills those
requests past the monthly free credit, so they are debounced and cached
per input.
| keys | action |
|---|---|
enter |
Open the place in the chosen app; on a route row, the directions |
cmd+enter |
Directions from here (the current location) |
cmd+h |
Directions from home |
cmd+w |
Directions from work |
cmd+c |
Copy the address |
cmd+l |
Copy the link (a web url, never maps://) |
cmd+shift+o |
Open in the other app |
tab |
Next travel mode |
Settings, [extensions.maps]:
| key | type | default | what |
|---|---|---|---|
app |
google, apple |
google |
Which app the rows open; Apple Maps is macOS only. |
home |
text | empty | The Home row, home in a route, "from home" directions. |
work |
text | empty | Likewise; both set lists the commute. |
places |
list | empty | Name = address per line; a line without = is both. |
api_key |
secret | empty | Places API (New) key, for autocomplete. |
For the tests, PAL_MAPS_PLACES replaces the Places host.
Speedtest (speedtest, speedtest-history)
A speed test watched in the panel, run by the CLI installed, from
speedtest/. A view palette: opening it draws the last result
and which tool was found (Speedtest by Ookla, speedtest-cli or fast,
in that order for auto; speedtest on PATH is told apart by its
--version, since sivel's package installs an alias of that name), or
the three install lines when none is; nothing runs until Enter.
Enter spawns the tool in its own process group and the view follows its
stream through view.update: a headline with the figures so far (else
what it is doing, or what to do), a bar per direction with the live figure
in Mbps (Ookla's own fraction fills it; the other two get an estimate
from the elapsed time that stops short of full), the ping tiles (latency,
jitter, loss when reported), the server and the ISP in the head, the
elapsed time in the foot with a tick between the tool's lines. Enter
while it runs stops it (SIGTERM to the group, SIGKILL a second later);
a test past two minutes is stopped. ⌘Enter copies the result as
one line, ⌘O opens Ookla's result page, ⌘H opens the history;
Escape leaves and a running test keeps running, the view catching up
when reopened (on: ["show"] re-asks it, and a push lands when the
level reports itself).
Speedtest History (speedtest-history, live): every finished run,
newest first, the last keep in storage, the figures and the ping as
the name, the server, ISP and tool as the subtitle, the date on the
right; Enter copies the line, ⌘O opens the result page, ⌘D
removes it, the last row clears after a confirm card. The first row,
Trend, is a view of the last twenty runs as bars, download in blue and
upload in green, each against the best of its own, the ping beside;
Enter there copies the history as text.
| keys | action |
|---|---|
enter |
Start the test; while it runs, stop it |
cmd+enter |
Copy the result |
cmd+o |
Open the result page (Ookla) |
cmd+h |
History |
cmd+d |
History: remove the run |
Settings, [extensions.speedtest]:
| key | type | default | what |
|---|---|---|---|
tool |
auto, ookla, speedtest-cli, fast |
auto |
Which CLI runs the test. |
server |
text | empty | A speedtest.net server id; fast ignores it. |
keep |
1 to 200 | 30 |
Runs the history holds. |
For the tests, PAL_SPEEDTEST_PATH names a directory searched first for
the three binaries.
YouTube (youtube-search, youtube-channels, youtube-later)
YouTube searched from the panel, from youtube/. An input
palette: 400 ms after the last key the videos come back with the
thumbnail (i.ytimg.com, no key) as the icon and channel · length · views · age as the subtitle (live now for a stream); nothing typed is
the trending list for region under a Trending section; yt: lofi at
the root answers inline, and a root query nothing matched offers "Search
YouTube for …". Enter opens the video in the browser, ⌘Enter plays
it in IINA, mpv or VLC (player; auto takes the first installed, the
browser when none is; mpv and VLC need yt-dlp), ⌘C copies the url,
⌘S keeps it, ⌘⇧O opens the channel; the detail pane shows
the bigger thumbnail, the channel as a link, the exact views and the
date.
YouTube Channels (youtube-channels, input): channels by name with the
avatar, the subscriber count and the description; Enter lists the
channel's latest videos as a level of the search palette (what is typed
there filters them), ⌘Enter opens the channel. Subscriptions are not
listed: they need a Google sign-in (OAuth), which pal does not do, and
the empty palette says so.
Watch Later (youtube-later, live, its rows at the root): what ⌘S
kept, newest first, the last 200 in storage, the same actions and
⌘D to remove; the last row clears it after a confirm card.
Backends: the Data API v3 with api_key (a Google Cloud key with the
API enabled; 10,000 units a day, a search 100, so about a hundred
searches), or an Invidious instance at invidious_url through
/api/v1 with no key; the key wins when both are set. Most public
Invidious instances have turned the API off (on 2026-09-17 only
invidious.f5.si of the listed ones answered), so a self-hosted one is
the reliable choice; a refused key, a used-up quota, an instance that
answers HTML or an error is one row naming the fix.
| keys | action |
|---|---|
enter |
Open in the browser; on a channel, its latest videos |
cmd+enter |
Play in the player; on a channel, open it |
cmd+c |
Copy the url |
cmd+s |
Watch later |
cmd+shift+o |
Open the channel |
cmd+d |
Watch Later: remove |
Settings, [extensions.youtube]:
| key | type | default | what |
|---|---|---|---|
api_key |
secret | empty | The Data API v3 key. |
invidious_url |
text | empty | An Invidious instance serving its API. |
player |
auto, iina, mpv, vlc, browser |
auto |
What cmd+enter plays in. |
region |
text | empty | A two-letter country code for the trending list. |
For the tests, PAL_YOUTUBE_API replaces the Data API host (the
Invidious host is the setting) and PAL_YOUTUBE_PATH a directory
searched first for the players.
Screenshots (screenshots)
Take a screenshot from the panel, then find the ones you took. A live,
primary palette: three Capture rows lead it, under them the screenshots
folder newest first, each row with a 48 px thumbnail (the app's icon://
file route), the pixel size read off the PNG header, the file size and
the age. The folder is read again on every show, so the shot just taken
is there, and its name finds it at the root.
The Capture rows run the OS tool once the panel is down (the pick answers
hide and the tool starts a beat later, so the panel is never in the
shot): screencapture -i (an area; space switches to a window, Escape
cancels), screencapture -i -W (a window) and screencapture (the
screen) on macOS; grim -g "$(slurp)" and grim on Linux, where both
tools must be on PATH (and wl-copy for the clipboard) or the section is
one hint row. Enter sends the shot where the destination setting says,
a file named the way macOS names its own (Screenshot 2026-09-17 at 14.03.22.png) in the folder, or the clipboard; ⌘C on the row takes
the other destination for that one shot; ⌘Enter waits timer
seconds first (-T). The HUD then says Screenshot saved: <name> or
Copied to the clipboard; a cancelled capture says nothing. The shutter
is silent unless sound is on.
| keys | action |
|---|---|
enter |
Open (marked rows: every one); on a Capture row, capture |
cmd+enter |
Reveal in Finder / the file manager; on a Capture row, capture after the timer |
cmd+c |
Copy image: the file itself onto the clipboard, so a paste in a chat drops the picture; on a Capture row, the other destination |
cmd+shift+c |
Copy the path |
cmd+m |
Copy as markdown image:  |
cmd+shift+t |
Copy text (OCR): the text in the picture through the core's OCR (Vision on macOS, tesseract on Linux); ocr_concealed keeps it out of the history |
cmd+d |
Move to Trash (asks first; marked rows together) |
tab, x, shift+↓, cmd+click |
Mark rows |
Open, Reveal, Copy image, Copy path and the trash take marked rows
(multi). The detail pane (⌘I) leads with the folder over the name
and the pixels and size large, then the picture itself and the time it
was taken. The root's Now
section offers a screenshot taken in the last two minutes as
Screenshot taken 40 s ago with Open, Copy image and the markdown tag.
For the tests, PAL_SCREENCAPTURE_BIN names a stand-in capture tool and
PAL_SCREENSHOTS_TRASH a stand-in trash.
Settings, [extensions.screenshots]:
| key | type | default | what |
|---|---|---|---|
destination |
file, clipboard |
file |
Where Enter on a Capture row sends the shot. |
folder |
folder | (system) | Where captures land and the list reads. Empty: defaults read com.apple.screencapture location, else ~/Desktop; on Linux ~/Pictures/Screenshots when it exists, else ~/Pictures. |
timer |
1 to 60 | 3 |
Seconds the delayed capture waits. |
sound |
boolean | false |
Play the shutter sound. |
all_files |
boolean | false |
Every image and recording in the folder, not only the ones named like macOS's captures (Screenshot, Screen Shot, Screen Recording, grim-). |
limit |
1 to 500 | 50 |
At most this many recent rows. |
ocr_concealed |
boolean | false |
Text copied by OCR is concealed. |
WhatsApp (whatsapp-chats, whatsapp-unread, whatsapp-search, whatsapp-contacts, whatsapp/unread)
One extension, four palettes and a bar item over a self-hosted
OpenWA gateway's HTTP API (the owner's at
http://wp.lan, one session named main linked to his phone, the
archive of every message since 2018 behind its /api/search).
whatsapp/README.md has the setup for a gateway of your own.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Chats | whatsapp-chats |
live, lazy, primary | opens the chat in the app or the web client |
| Unread | whatsapp-unread |
live, lazy | the same, over the unread chats only |
| Search WhatsApp | whatsapp-search |
input | opens the hit's chat |
| Contacts | whatsapp-contacts |
indexed, 1 h, catalog | opens a chat with the contact |
Chats. GET /chats (the list, live from WhatsApp), the status feed,
broadcast lists and Channels dropped, the unread chats first then newest
first. The unread chats' newest messages (their count, five at most)
come from /messages/<chat>/history (live, fromMe, oldest first) laid
over /messages?chatId= (the archive: the sender's saved name, the
quoted reply), merged by message id, so the row says who said what and
a media message is named ([photo], [voice message], [document] name; what the gateway cannot classify is its text, else [message]);
the other rows carry the gateway's one-line summary. The row:
the profile picture (else the initial on a tile; a group the group
glyph), the name, the line, a group tag, the unread count in green,
the time. The pane: the last twenty messages as a conversation (the
sender in bold, "You" for yours, the time, a quoted reply as a
blockquote, media in brackets), then kind, unread, the phone (a LID-era
@lid id resolved through /contacts/<lid>/phone, once), the newest
message's time, a link into the web client. Pictures are never on the
listing's path: the gateway resolves them at ~150 ms each (50 ids took
11.5 s live), so a background pass asks for eight at a time among the
newest 48 chats and keeps the urls (good for about nine days, oe in
the query) in storage under pictures. One chat list serves the bar
and the palettes for 30 s.
| action | shortcut | when |
|---|---|---|
| Open chat | Enter |
whatsapp://send?phone= in the desktop app, https://web.whatsapp.com/send?phone= on the web, per open (auto is the app when installed on macOS); a group has no link of its own, so WhatsApp opens at the top and the HUD says so |
| Mark as read / Mark as unread | ⌘Enter |
POST /chats/read and /chats/unread, over the marked rows too |
| Send a message | ⌘⇧R |
send on; a form with the text and a box to quote the latest message (/messages/reply, else /messages/send-text) |
| React to the latest message | ⌘⇧E |
send on; WhatsApp's six quick reactions or remove yours (/messages/react) |
| Open in the web client | ⌘⇧O |
|
| Copy number / Copy name | ⌘C |
+905...; a group's name |
Unread: the same rows over the chats with something unread, direct
messages then groups (the root's unread rows). Search WhatsApp:
/api/search?q= (full-text over the archive, <mark> snippets), 300 ms
after the last keystroke, 40 hits; the row is the matching line, who
said it where (the chat's name from the list, a sender's from the
contacts), when; the hits whose chat is already named show as soon as
the search answers, in order up to one whose chat waits on its contact;
Enter opens the chat, ⌘C copies the message, the
pane is the chat's conversation; a provider that is down is one hint
row with the status and the gateway's message. Contacts: /contacts
a thousand a page (the first page, then five in parallel), the saved
ones (isMyContact with a name) one per number, an hour in memory; a
name without a letter or digit, or that is the number itself, reads as
the number alone; Enter opens a chat, ⌘C copies +<number>, ⌘⇧C a vCard 3.0.
send, off by default. Off is read and mark-read only: no Send a
message, no React, no reply field in the popover; every write path
checks it again at pick time and refuses with a toast naming the
setting. The key can do all of it; the setting is the control (the
owner's rule for sends: an outward action, only as a form he submits).
The bar item whatsapp/unread: the number of unread chats as the
badge, hidden at zero (the core's show = "always" keeps the glyph and
lists the recent chats in the popover), red while a direct chat is unread
(its dm rule); every 120 s and on show, wake, network. The popover is a
view of the item's own (view.ts): direct messages then groups with the
picture as a data url, the newest message and the time, a cursor the
arrows move and a click sets; Enter opens, m marks read, a all,
r (send on) a message field whose Enter sends, o WhatsApp, p the
Unread palette, ⌘⇧O the web client.
Popover keys of whatsapp/unread (rendered every 120 s and on show, wake,
network; the arrows move the cursor, a click sets it):
| keys | does |
|---|---|
enter |
Open the row's chat |
up |
Move the cursor (down, j, k too; a click sets it) |
r |
Reply: the search row becomes the field, Enter sends (send on) |
m |
Mark the row read |
a |
Mark all read (cmd+shift+a too) |
o |
Open WhatsApp |
p |
Open the Unread palette |
cmd+shift+o |
Open the row's chat in the web client |
Links: pal://whatsapp/open?chat=<id | phone | name> (a chat id, a
number, or a name from the chats then the contacts, exact then prefix)
and pal://whatsapp/search?q=.
Settings, [extensions.whatsapp]:
| key | type | default | what |
|---|---|---|---|
base_url |
text | http://wp.lan |
Where the gateway answers; /api is under it. |
api_key |
secret | (none) | Sent as x-api-key; in the OS keychain. |
session |
text | main |
The session's name; the UUID is resolved once and kept in storage (session:<name>), resolved again on a 404 naming the session. |
send |
boolean | false |
Send a message, React, the popover's reply field. |
open |
auto, app, web |
auto |
Where a chat opens. |
Hint rows: no key (Open WhatsApp settings), the gateway unreachable at
its url, the key rejected (401), a session not ready (a qr_ready one
says to scan the code at the gateway's url; the chat list answers 409
meanwhile), a session name nobody has, a 429 (refused locally for
Retry-After, 60 s without), the search provider down. Not shown: muted
and pinned chats and a group's member count (the gateway's chat summary
has none of them).
Images (images)
Compress, resize, convert, rotate, crop, strip metadata, make an icon
set, read the text, from images/: what Raycast's TinyPNG and
Image Modification do, local first. An input palette whose rows are the
images at hand: before you type, the Finder selection (a selected folder's
images too), an image or a copied file list on the clipboard, and the
Results of this session; a typed path lists a file, a folder's images or
the completions of the last segment; anything else filters by name. With
more than one image an "All N images" row leads, and rows mark with tab
(or x with nothing typed). Each row shows a thumbnail (the core's own
for PNG, JPEG and GIF, sips -Z or ImageMagick into the cache for the
rest), the size and the pixel size; the detail pane (open by default) is
the picture at 256 px over the info: dimensions, format, colour space and
profile, camera, lens, exposure, date and location (sips -g all, or
identify; exiftool for the photo fields when installed).
Every operation writes next to the source with a suffix (-compressed,
-web, @0.5x, -800w, -rotated90, -square, -padded, -stripped,
-gray; a convert swaps the extension; @2x strips an existing @Nx
and half of a @2x stem is the bare name; a taken name gets -2), then
copies the result's path (the paths one per line for a batch; the image
itself when the input was the clipboard, whose result goes to the cache's
clipboard/ folder), hides and says in the HUD what it did and with
which tool: "Compressed photo.png: 1.4 MB → 312 KB (−78%), pngquant ·
path copied". A file no encoder can shrink writes nothing and says so. A
batch past eight seconds answers with a progress toast and finishes behind
the panel. With replace on the result takes the source's place and the
source is copied to ~/Library/Caches/pal/images/originals/ first;
Restore original puts it back. Nothing is ever deleted: a result written
next to its source can go to the Trash, the source stays.
The encoders, in the tools order (one left out is never used; every
result names the one that made it): pngquant (lossy PNG) and oxipng
or optipng (lossless), cjpeg (mozjpeg's or libjpeg-turbo's, through
djpeg) and jpegtran, cwebp, avifenc, gifsicle, exiftool, then
sips and ImageMagick. sips (in every macOS) does the geometry whenever it
is listed and writes PNG, JPEG, TIFF, GIF, BMP, PDF, HEIC, AVIF and ICO;
it never compresses a PNG (it re-encodes a palette PNG as RGBA). ImageMagick
is the fallback for everything and the whole of it on Linux. Strip
metadata is pal's own for PNG and JPEG (lossless: the APPn and COM
segments, the text, time, EXIF and ICC chunks). The empty listing names
up to three encoders worth installing. Optimise for web (cmd+enter) caps
the long side at web_max, encodes at quality (as web_format), strips,
and opens a view with a row per image (thumbnail, before → after, the
saving as a tag, a bar) under the total saved as its headline, following a long batch
as it lands. Make an icon set writes name-icons/ with the .iconset,
the .icns (iconutil), favicon.ico and the touch and Android sizes.
TinyPNG (tinypng_api_key) adds cmd+t for PNG, JPEG, WebP and AVIF.
| keys | action |
|---|---|
enter |
Compress |
cmd+enter |
Optimise for web |
cmd+l |
Compress losslessly |
cmd+t |
Compress with TinyPNG (with a key) |
cmd+shift+r |
Resize… (presets; or type 800, x600, 800x600, 50%, 2x) |
cmd+shift+v |
Convert… (PNG, JPEG, WebP, AVIF, HEIC, PDF, TIFF, GIF; a missing tool named) |
cmd+shift+o |
Rotate or flip… |
cmd+shift+a |
Crop or pad… (1:1, 16:9, 4:3, 3:2, 9:16 centred; pad to a square) |
cmd+shift+m |
Strip metadata |
cmd+g |
Grayscale |
cmd+shift+f |
Make an icon set |
cmd+shift+t |
Copy text (OCR) |
cmd+shift+i |
Copy info |
cmd+c |
Copy path |
cmd+shift+p |
Copy image (PNG and JPEG onto the pasteboard; other formats as a file) |
cmd+o, cmd+shift+e |
Open, Reveal in Finder |
cmd+shift+z |
Restore original (a replaced result) |
cmd+d |
Move result to Trash (asks; the source stays) |
Settings, [extensions.images]:
| key | type | default | what |
|---|---|---|---|
replace |
boolean | false |
Write over the source; a copy is kept for Restore original. |
quality |
1 to 100 | 80 |
For the lossy encoders (pngquant's floor is 25 under it). |
web_format |
keep, webp, avif |
keep |
What Optimise for web writes. |
web_max |
100 to 10000 | 2000 |
Optimise for web caps the long side at this many pixels. |
thumbnails |
boolean | true |
Thumbnails on the rows and in the pane. |
pad_color |
hex | #ffffff |
What Pad to a square fills with. |
tinypng_api_key |
secret | (none) | Adds Compress with TinyPNG. |
tools |
list | every tool | The order the encoders are tried. |
Stats (stats, stats/cpu, stats/memory, stats/disk, stats/network, stats/load)
CPU, memory, disk, network and load as five bar items off one sampler
(every interval seconds, 3 by default; about 5 ms a tick, 27 ms while a
process popover is open), each hidden by its rules while quiet and
coloured past its thresholds, with a popover of the breakdown; the same
facts as rows in the Stats palette. Detail: stats/README.md.
| item | strip (its label setting) |
quiet, hidden | amber | red |
|---|---|---|---|---|
stats/cpu |
42%; ▂▃▅▇▆; one bar per core; 42% · node |
under 70% | from 70% | from 90% |
stats/memory |
63%; 24.2 GB; 14.4 GB free; sparkline |
under 80% with no pressure | 80%, or pressure warn |
90%, or critical |
stats/disk |
the startup volume's free space; share; used | over 20 GB free, every writable volume under 85% | 85% or 20 GB | 95% or 5 GB |
stats/network |
↓1.2M ↑80K; ↓1.2M; sparkline |
under 1 MB/s either way | (blue from 10 MB/s) | |
stats/load |
3.26; 3.26 3.25 2.91 |
the 1 min load under the core count | from the cores | from twice them |
The rules are the manifest's, edited by id in Settings > Bar or under
[bar.items."stats/cpu".rules.quiet]; show = "always" keeps an item on
the strip muted at rest, a quiet rule set to when = "false" keeps it in
colour. The states every render publishes (stats.cpu, stats.cpu_top,
stats.cores, stats.memory, stats.memory_pressure, stats.swap,
stats.disk, stats.disk_free (GB), stats.disk_worst, stats.net_down
and stats.net_up (KB/s), stats.load1, load5, load15) are what a
rule of your own reads.
The popovers, each with a sparkline of the last 60 samples in the theme's
ink, a cursor ring the arrows (or j/k) and a click move, and s for
the palette: CPU the share and level, cores / load / uptime, one bar
per core, the five busiest processes (Enter Activity Monitor, x kill
after a confirm, c copy, p Processes); Memory the segments bar
(app, wired, compressed, cached, free) with a legend, the swap, the five
largest, the same keys; Disk a card per volume with its bar, free
space, mount point and a read-only badge (Enter reveals in Finder,
c copies the path); Network the two rates, both in one sparkline,
every interface with its kind, SSID, address and rates (Enter the
Network palette, c copies the address); Load the three averages as
tiles, the per-core figure.
The palette is live (from the last sample, no tool runs; ⌘R samples
now) with the detail pane showing: Processor (CPU, load), Memory (memory,
swap), Disks, Network (each interface, then the totals), Busiest and
Largest processes, System (uptime). The value is the name.
| action | shortcut | what |
|---|---|---|
| Copy | Enter |
the value: the share, the usage, a volume's mount point, an interface's address, a process's pid |
| Open Activity Monitor | ⌘O |
the Processes palette on Linux |
| Open bar popover | ⌘P |
the row's item, through pal://bar/stats/<item> |
| Reveal in Finder | ⌘R |
a volume |
| All addresses | ⌘A |
the Network palette |
| Kill | ⌘⌫ |
a process, after a confirm |
Links: pal://stats/cpu (and memory, disk, network, load) opens
the popover; ?palette=1 the palette on that section.
Sources, no root: os.cpus() deltas, os.loadavg(), vm_stat and
sysctl (Activity Monitor's arithmetic and the kernel's pressure level),
netstat -ibn, df -kP and mount (every 60 s), ps (every 10 s, every
tick while a process popover is open) on macOS; /proc/meminfo and
/proc/pressure/memory, /proc/net/dev with ip -j -br addr,
/proc/mounts on Linux. No temperature (a native reader or root on
macOS).
Settings, [extensions.stats]:
| key | type | default | what |
|---|---|---|---|
interval |
number | 3 |
Seconds between samples, 1 to 60. |
disk_hide |
list | [] |
Mount points or volume names left out of the disk item and the palette. |
Each item's settings, [bar.items."stats/<item>".settings] (cpu,
memory, disk, network, load item settings): label, what the strip says.
| item | label |
default |
|---|---|---|
cpu |
percent, spark, bars, top |
percent |
memory |
percent, used, free, spark |
percent |
disk |
percent, free, used (the startup volume) |
free |
network |
rate, down, spark |
rate |
load |
one, three |
one |
Battery & Power (power)
The battery and what drains it, from power/. The level comes
from pmset -g batt (macOS) or upower (Linux); everything else from the
optional power watcher: its state.json, the samples.jsonl ring buffer
beside it and power blame <window> for watt-hours, so the apportioning is
the watcher's and computed once. One view: on the left the level with a
plain sentence (Drawing 12 W · 1 h 48 min left), six hours of draw (the
charger's stretches shaded, the charge level dashed), today's watt-hours
as a share of a full charge, and health, cycles and temperature as tiles.
On the right four tabs: Now (the watcher's warning, the draw split
between the apps and chip and the screen and the rest, the processes with
their watts and why: CPU, wakeups, disk, network), Today, 7 days
and All time (watt-hours per process with each share; a window spent
on the charger ranks by activity, since there is no draw to apportion).
Re-read every 10 s while open. Without the watcher only the level and the
time left show, and the view says what the watcher adds.
| keys | action |
|---|---|
enter |
Find the process in Processes (on Screen & the rest and the other bookkeeping lines: Battery settings) |
tab, shift+tab, left right, h l, 1–4 |
Switch tabs |
up down, j k |
Move the cursor |
cmd+c |
Copy the name |
s |
Open Battery settings |
cmd+r |
Refresh (drops the cached windows) |
The battery bar item is hidden while healthy (its manifest rules over
power/level, power/charging, power/draw and power/alert); hover
shows the popover (the level, the last hour, the warning, the top four,
today's watt-hours), a click opens the palette.
| setting | type | default | what |
|---|---|---|---|
power_state_file |
path | ~/.local/share/power/state.json |
The watcher's state; samples.jsonl is read from beside it. A state older than 3 minutes is ignored. |
odak (odak, odak-add, odak-search, odak-done, odak/today)
One extension, four palettes and a bar item over an odak
server, signed in with its API key. Everything is the server's own REST
API (GET /todos once, cached 60 s and shared by the palettes and the
bar; /sections; the writes). odak/README.md has the
endpoint list and the two model quirks worked around (an id is a hash of
the line, so every edit gives a new one, computed here as odak does; a
day cannot be cleared).
| palette | id | kind | what Enter does |
|---|---|---|---|
| Todos | odak |
live, 60 s | completes the todo (Tab marks several) |
| Add Todo | odak-add |
input | adds the line as read |
| Search Todos | odak-search |
input | completes an open todo, reopens a completed one |
| Completed | odak-done |
live, 60 s | reopens |
Signing in. url is the server (http://host:8761, the same origin
as the web UI), api_key its ODAK_API_KEY (kept in the keychain).
Without either, every palette is one hint row naming which and opening it
in Settings; a refused key says so; a server that does not answer names
the address and ⌘R, and rows already fetched stay through an outage.
Todos. Two commands (New todo, the text typed in the bar as an
argument, ⌘Enter into Add Todo; Open odak), then Overdue and Due today,
then the file's sections in order, a subtask under its parent, Not yet at
the end for a [w:date] still ahead. A row carries its tags and its day
as chips (overdue 3 d red, today amber, tomorrow blue, Fri within
the week, the date beyond), a red mark when urgent, the subtask count; the
pane the subtasks as a task list and the metadata. Complete (Enter, a
toast and an Undo row for a minute), Edit… (⌘E), Snooze to tomorrow
(⌘T), Snooze… (⌘S: tomorrow, in 3 days, next Monday, next week, in a
month, or a day typed), Move to section… (⌘M), Mark urgent (⌘U), Add
subtask… (⌘N), Open link (⌘L), Open odak (⌘O), Copy text (⌘C),
Copy link (⌘⇧C), Delete (⌘D, asks). The root's Now section shows what
is overdue or due today.
Add Todo. One line, read back as you type: #tag, a bare ! for
urgent, /section (a prefix of one of the file's), d:day / w:day, and
a day in words at the end (tomorrow, fri, next mon, in 3 days,
20 sep, 2026-10-01, by fri; a time after it stays in the text).
Enter adds and hides with the HUD line, ⌘Enter keeps the line, ⌘⇧C
copies it. Before you type: what was just added (Undo deletes it), the
selection and the clipboard as todos. A root query with no hit offers
"Add “…” to odak". Search Todos: every word against text, tags and
section, open by section then completed. Completed: what is checked
off by section, with when for the ones completed from pal; Enter reopens.
The bar item odak/today: the count of open todos due today or in
its today_sections plus the overdue, red while any is overdue (rule
overdue), hidden at zero (rule quiet; show = "always" keeps the
glyph), every 300 s and on show, wake, network; facts odak/overdue,
odak/today, odak/open. The popover: the overdue first, then today's,
a cursor the arrows move; Enter/x completes, u the flag, t
tomorrow, n a field whose Enter adds, o odak, p the palette, r
fetches again. pal://odak/add?text=... adds from outside (section,
due, tags, urgent on top).
Settings, [extensions.odak]:
| key | type | default | what |
|---|---|---|---|
url |
text | (none) | The server's origin. |
api_key |
secret | (none) | ODAK_API_KEY. |
default_section |
text | Inbox |
Where a todo lands unless the line names a section (one of the file's, else Inbox). |
today item settings, [bar.items."odak/today".settings]:
| key | type | default | what |
|---|---|---|---|
today_sections |
list | ["Focus", "Today"] |
What the item counts as today's, on top of what is due today. |
For the tests, PAL_ODAK_URL and PAL_ODAK_KEY replace the two settings.
Grafana (grafana, grafana-alerts, grafana-query, grafana/alerts)
Grafana over its HTTP API with a service account token (Viewer reads
everything here; Editor is needed to silence or star); multi, so a
second Grafana is [instances."grafana@work"] with its own url and
token (Config).
Grafana Dashboards (grafana) is every dashboard the token can see,
starred first, then as Grafana sorts them. A row is the title; the
subtitle the description, or the folder when there is none; the folder
is a blue tag and the first two tags follow it; the uid, the tags and
the folder are keywords. Enter opens it in the browser with the
time_range setting appended (from=now-6h&to=now) when set, ⌘Enter
asks for a range in a form (from, to, kiosk), ⌘F opens it in kiosk
mode, ⌘S stars or unstars it (the token's own stars, which are what
sort first here), ⌘C copies the URL, ⌘U the uid, → drills into the
dashboard's folder. The filter (Tab) is All, Starred, Folders; Folders
lists every folder with a dashboard in it and Enter drills in. Listed
every 15 minutes. The detail pane (lazy) shows the description, the
folder, the tags, the time range and refresh, the panels by type, and
the first three time series panels (sparklines) drawn: as panel images
when the image renderer plugin is installed (probed once per run), else
as sparklines the extension draws from the panels' own queries in one
/api/ds/query call over the dashboard's range, the first series' last
value in the panel's unit as the caption.
Grafana Alerts (grafana-alerts) is live: every alert instance
firing or pending from the ruler's Prometheus-compatible API, marked
where the Alertmanager says a silence covers it. Firing first, then by
rule and labels. A row is the rule's name; the subtitle the rendered
summary (else the labels); a red firing or amber pending tag, the
severity, a silenced tag, since when. The pane has the summary and
description, the rule (a link), the state, the labels, the value, the
rule's health when it is not ok, the silence and the dashboard the rule
points at. The filter is Firing and pending, Firing, Pending, Silenced
(the covered instances, then the active silences, each with Expire).
Enter opens the rule, ⌘Enter its dashboard, ⌘S / ⌘⇧S / ⌘D
silence the instance for 1 hour / 4 hours / 1 day (asking first; the
matchers are the rule's uid plus the instance's own labels), ⌘E expires
the silence, ⌘C copies the summary, ⌘L the labels. Listed on show,
not twice within 30 s.
Grafana Query (grafana-query) is an input palette: what you type
runs as a PromQL instant query against the Prometheus datasource
(datasource by uid or name, else Grafana's default), one row per
series with Grafana's display name, the value on the right and the
labels in the pane; a parse error is a calm hint with Prometheus's
message. Before you type, the saved queries (queries, Name = expr
lines): Enter runs one, ⌘⌫ removes it. On a result Enter copies
the value, ⌘Enter the series with its value, ⌘⇧A every series, ⌘C
the expression, ⌘O opens Explore with it, ⌘S saves it under a name.
Bar item grafana/alerts: the firing count in red and the pending
count in amber, hidden by its rules while both are zero (a firing
instance under a silence does not count); grafana/firing and
grafana/pending as states. Refreshed every minute and on show, wake and
the network back. The popover lists the instances by state with a colour
rail: Enter opens the rule, o the dashboard, s / f / d silence
for 1 hour / 4 hours / 1 day (asking first; s on a silenced one
expires it), c copies the summary, a opens the alert list, p the
palette, r refreshes.
Links: pal://grafana/query?expr=up%20%3D%3D%200 opens the prompt with
the expression typed; pal://grafana/open?uid=<uid>&from=now-24h&kiosk=1
opens a dashboard.
When something is wrong every palette is one inert hint row that says
what and where to fix it (Settings › Extensions › Grafana; the no-URL
row's Enter opens it there): no URL, a URL without a scheme, no token,
a keychain:/env: token that did not resolve, a rejected token (401),
a host that did not answer within the timeout, or one that could not be
reached. A redirecting URL is followed with the token kept and logged.
A silence or a star a Viewer token may not make is a failure toast
naming the role.
Settings, [extensions.grafana]:
| key | type | default | what |
|---|---|---|---|
url |
text | unset | Where Grafana answers, scheme included. |
token |
secret | unset | A service account token. A keychain: or env: reference in the file. |
time_range |
text | unset | Appended to a dashboard's URL as from=…&to=now when set; empty opens the dashboard's own range. |
datasource |
text | unset | The Prometheus datasource the prompt runs against, by uid or name; empty is Grafana's default. |
queries |
list | five Name = expr lines |
The saved queries listed before you type. |
sparklines |
number | 3 |
How many time series panels the pane draws; 0 turns it off. |
timeout |
number (s) | 8 |
How long one request may take. |
Diff (diff, diff-pick)
Diff what you copied, from diff/. A view palette: opened
bare it compares the two newest text entries of the clipboard history,
the older on the left, so the diff reads as what changed on the way to
the newer copy. It leads with how much changed as a headline ("12 lines
added, 4 removed", "No differences"); the header under it names both
sides with when and where they were copied (whitespace only amber when
that is all that changed); under it the lines on a
sunken surface, unified by default: both line numbers, the sign, the
text in monospace, removed lines on a red tint and added ones on a
green one (consecutive lines of one kind share the block), the words
that differ inside a changed pair on a stronger tint, a pair that
differs in whitespace alone tagged. s lays the two sides out side by
side. Runs of unchanged lines beyond the three of context fold to one
row ("⋯ 16 unchanged lines"); tab walks the folds, space or a
click expands one, a all. w ignores whitespace as diff -w does,
x swaps the sides, enter copies the unified diff, l and r the
left and right text, o opens the pair in an external tool (tool:
VS Code, kitty's diff kitten, FileMerge, Meld, or a custom command with
{left} and {right}; the sides written under the cache when they are
not files). Other pairs: n the two newest copies again, e the
clipboard against the selection in the app in front, h two entries
picked from the history, f two files from a form (a missing file, a
folder or a binary refused under its field; over 1 MB left to the tool).
Nothing to diff (one copy in the history, nothing selected) is a view
that says so and lists the ways in.
Diff from History (diff-pick, input, multi): the history's text
entries; enter on one takes it as the left side and lists the rest
for the right, or two marked (tab, x) and enter diff at once. The
Clipboard History palette has the same two actions on a text entry,
Diff with… and Diff these two (cmd+shift+f).
| keys | action |
|---|---|
enter |
Copy the unified diff |
o, cmd+enter |
Open both sides in the external tool |
s |
Side by side, or unified |
w |
Ignore whitespace, or mind it |
x |
Swap the sides |
l, r |
Copy the left, the right text |
tab, shift+tab, space, a |
Next fold, previous, expand it, expand all |
n, e, h, f |
The newest copies, clipboard vs selection, pick from history, two files |
Links: pal://diff/clipboard, pal://diff/selection,
pal://diff/files?left=&right=, pal://diff/text?left=&right=
(left_label, right_label), pal://diff/history?left=<id>&right=<id>.
Settings, [extensions.diff]:
| key | type | default | what |
|---|---|---|---|
tool |
auto, code, kitty, opendiff, meld, custom |
auto |
What o opens the sides in; auto is the first installed. |
tool_command |
text | empty | With custom: the command line, {left} and {right} the two files. |
The diff is jsdiff (diff on npm). For the tests, PAL_DIFF_PATH is
searched for the tools and PAL_DIFF_CACHE takes the written sides.
Turkish (turkish)
Turkish text tools, from turkish/. An input palette: what
you type (with nothing typed, the selection in the app in front, else
the clipboard's text) converted five ways, one row each, named by the
result with the conversion and the count of characters it touched
beside it (unchanged as a tag when none): Deasciified (Turkce yazilmis bir cumle → Türkçe yazılmış bir cümle, the letters decided
by context: sik → sık, kus → kuş, acik → açık, yas →
yaş; Deniz Yüret's Emacs turkish-mode pattern table in Mustafa Emre
Acer's JavaScript port, the turkish-deasciifier package), Asciified
(the reverse table), UPPERCASE and lowercase the Turkish way (i → İ,
ı → I and back, the tr locale), Title Case (every word, a suffix
after an apostrophe kept down: İstanbul'da). enter pastes the row
into the app in front, over the selection when that is where the text
came from; cmd+c (and cmd+enter) copies; cmd+t hands the row to
Translate; cmd+shift+c copies the source. The detail pane holds the
whole converted text over the source.
| keys | action |
|---|---|
enter |
Paste (over the selection the rows came from) |
cmd+enter, cmd+c |
Copy |
cmd+t |
Translate the row |
cmd+shift+c |
Copy the source text |
Links: pal://turkish/deasciify?text=… (and asciify, upper,
lower, title) copies the result, paste=1 pastes it; without text
the selection (else the clipboard) is converted and, from a selection,
pasted over it: a keybind that fixes the letters of what was just typed.
Settings, [extensions.turkish]:
| key | type | default | what |
|---|---|---|---|
primary_action |
paste, copy |
paste |
What enter does on a row. |
Not done: a {turkish …} snippet placeholder (sdk/src/placeholders.ts
has a fixed grammar with no hook for an extension's source).
Immich (immich, immich-albums, immich-people, immich-memories)
The Immich photo library from the panel, from immich/. An
input grid over Immich's CLIP search: what is typed goes to
/search/smart 300 ms after the last key ("a receipt", "dog on the
beach"), a file name (DSC00500, IMG-20260418-WA0021.jpg) to
/search/metadata by name, and nothing typed lists the newest uploads
under a Recent section. Every tile is Immich's own thumbnail rendition
(WebP, ~10 KB, fetched once into ~/Library/Caches/pal/immich,
$XDG_CACHE_HOME/pal/immich, and sent as a data url), captioned with
the place and the day ("Serdivan · 3 May 2022"; a video leads with "▶"
and its length). since:2025, before:2024-06, in:2023,
in:Ataşehir, type:video and is:favourite / is:archived narrow
either search; the dropdown (Tab) limits to photos, videos,
favourites or the archive; a More tile pages on, 24 at a time. Every
search sends visibility, since v3 lists archived and hidden assets
when it is left out. The pane (open by default) is the preview
rendition large over when and where (the place a link to the map), the
camera, lens and exposure, the size, the file, the people, the albums
it is in and the tags, fetched lazily per photo.
Enter opens the photo on the web address (web_url, else url);
⌘Enter copies the link; ⌘⇧C copies the picture itself (the
preview JPEG through copyImage, or as a file); ⌘Y Quick Looks it
(macOS); ⌘S downloads the preview and ⌘⇧S the original (fetched
behind the panel, the HUD says when it landed) into download_to as
<day>_<name>; ⌘F favourites or unfavourites; ⌘⇧A opens the albums
as a picker (New album… first) and Enter there files the photos; ⌘⇧F
copies the file name. The downloads, Favourite and Add to album take
marked rows (⌘-click, ⇧↑↓). A root query nothing matched offers
"Search Immich for …".
Immich Albums (immich-albums, indexed, 5 minutes, lazy): every
album with its cover, count, date range and a shared tag, the ones
with photos first (changed last first), the empty ones last; Enter
opens the album as the grid, where a search runs inside it; ⌘Enter
opens it in Immich, ⌘C copies the link. A key that may read the
statistics gets an "Immich library" row on top (photos, videos, bytes
on disk). Immich People (immich-people, indexed, 10 minutes):
the named people, favourites first, with their face; Enter lists
their photos, a last row counts the faces without a name. On This
Day (immich-memories, live, 30 minutes): Immich's memories for
today, a row per past year with the count, the places and the first
picture; Enter lists that year's photos.
pal://immich/search?q=…&filter=videos, pal://immich/album?name=…
(or id=) and pal://immich/person?name=… open the grids from a
script or a keybind.
| keys | action |
|---|---|
enter |
Open in Immich |
cmd+enter |
Copy link |
cmd+shift+c |
Copy image |
cmd+y |
Quick Look (macOS) |
cmd+s |
Download preview |
cmd+shift+s |
Download original |
cmd+f |
Favourite or unfavourite |
cmd+shift+a |
Add to album… |
cmd+shift+f |
Copy file name |
cmd+i |
The detail pane |
tab |
Photos, videos, favourites, archive |
Settings, [extensions.immich]:
| key | type | default | what |
|---|---|---|---|
url |
text | empty | Where the Immich API answers. |
api_key |
secret | empty | An API key from Account Settings › API Keys. |
web_url |
text | empty | Where links open when the site has another name than the API; empty: url. |
download_to |
folder | ~/Downloads |
Where the downloads go. |
[palettes.immich.settings] columns (3 to 10, default 6) is the tiles
per row. Without url or api_key every palette is one row that opens
Settings on the missing field; a refused key, a missing permission (a
403 names it) and an unreachable server each say so in one row or a
toast. For the tests, PAL_IMMICH_CACHE moves the cache directory and
PAL_COPY_IMAGE stands in for the clipboard copy.
Disk Space (space, space-map, space-largest, space-folders, space-cleanup)
A big file finder and cleanup, from space/. Disk Space
lists where to look: Home, every mounted volume with its free space
(/Volumes/* on macOS; /, /home and the media mounts on Linux),
the folders scanned before with their size and age, Scan a folder…
with the path as a typed argument in the bar, and Cleanup suggestions.
Enter opens Disk Map, a view level: the root scanned (a parallel
Bun walk, ~7 s for a million-file home, the map drawn live as it
runs) and drawn as a squarified treemap of boxes inside boxes, area by
size, a folder's own children faintly inside it, the same children as
rows beside the board with size and share, the focused box described
on a footer line. A scan is packed into storage when it lands (the
biggest nodes under 56 KB per root, four roots), so the next open draws
at once, marked "scanned 2 h ago"; a zoom into a folder the saved tree
cut rescans that folder alone. Boxes are coloured by the kind that
weighs most below them (the colour setting, or c: by depth instead),
sizes are the blocks on disk (sizes, or a: apparent).
| keys | action |
|---|---|
enter |
Zoom into the focused folder; open a file |
cmd+enter |
Reveal in Finder / the file manager |
backspace, - |
Zoom out |
up down left right, hjkl |
Focus the box in that direction |
tab, shift+tab |
Next / previous by size |
m |
Mark or unmark for the trash |
cmd+d |
Move to Trash: the focused box, or every marked one (asks, naming count and size) |
space |
Quick Look (macOS) |
cmd+o, cmd+c, i |
Open, Copy path, Info (a detail level: kind, sizes, count, share, modified, owner) |
cmd+l |
Largest files under this folder |
c, a |
Colour by kind or depth; sizes on disk or apparent |
cmd+r |
Rescan this folder; while scanning, stop |
A click on a box zooms into it (a file: focuses it), on a row focuses
it, on a crumb zooms out to it. Largest Files and Largest
Folders list the biggest under the last root (or the one a row or a
link names), the kind as a tag and a filter by kind on the files;
open, reveal, Quick Look (cmd+y), show in the map (cmd+m), copy
path, info (cmd+shift+i), trash (cmd+d, marked rows together).
Cleanup Suggestions measures what is usually safe to clear (user
and package caches, Xcode's DerivedData, node_modules and cargo
target folders untouched for stale_days from the scans in hand,
downloads older than that, the Trash) and offers one destructive action
per row (cmd+d), asked first, everything to the Trash (Empty Trash is
the one final action, and says so). Trash is Finder's delete on macOS,
gio trash on Linux; PAL_SPACE_TRASH names a stand-in for the tests.
Links: pal://space/scan?root=~/proj (rescan=1 scans again),
pal://space/largest?root=…. No bar item: free space is the stats
lane's. The research notes and the timings are in the extension's
README.
Settings, [extensions.space]:
| key | type | default | what |
|---|---|---|---|
sizes |
allocated, apparent |
allocated |
What the boxes and rows measure. |
colour |
kind, depth |
kind |
What colours a box. |
cross_devices |
boolean | false |
Walk into other file systems under the root (macOS firmlinks are always crossed). |
largest |
10 to 1000 | 100 |
Rows in the Largest lists. |
stale_days |
1 to 365 | 30 |
When a build folder or a download counts as stale. |
Theater (theater, theater-jellyfin, theater-jellyfin-search, theater-jellyfin-playing, theater-seerr-requests, theater-seerr-request, theater-radarr, theater-sonarr, theater-lidarr and their -add, -wanted, -history, theater-downloads, theater-downloads-history, theater-prowlarr, theater-prowlarr-search, theater-hydra-search, theater-navidrome, theater-navidrome-search, theater-abs, theater-abs-search, theater-kavita, theater-kavita-search, theater-shelfmark, theater-shelfmark-releases, theater-bazarr, theater/downloads, theater/playing, theater/requests, theater/queue)
One extension for a self-hosted media stack: Jellyfin, Jellyseerr, Radarr,
Sonarr, Lidarr, Prowlarr, NZBHydra2, Bazarr, SABnzbd, qBittorrent,
Navidrome, Audiobookshelf, Kavita, Shelfmark, Filebrowser and Homepage.
Every service is a settings group (a URL and an API key, or a user and a
password) and only the ones filled in light up: an unconfigured service is
one "Set up X" row in the Theater palette (Enter opens Settings on its
first empty field), its own indexed palettes list nothing, and its search
palettes say what to set. Each service speaks its own API (the endpoints
are in theater/README.md); one client (http.ts) folds the
auth in and keeps a small cache.
| palette | id | kind | what Enter does |
|---|---|---|---|
| Theater | theater |
live, 30 s | opens the service's web UI |
| Jellyfin | theater-jellyfin |
live, 60 s | opens the item in Jellyfin |
| Search Jellyfin | theater-jellyfin-search |
input | opens the item in Jellyfin |
| Now Playing on Jellyfin | theater-jellyfin-playing |
live, 30 s | opens the item |
| Play on | theater-jellyfin-play |
pushed | plays the item on that session |
| Requests | theater-seerr-requests |
live, 60 s | opens the request in Jellyseerr |
| Request a title | theater-seerr-request |
input | requests it (asks first) |
| Radarr, Sonarr, Lidarr | theater-radarr... |
live, 60 s | opens in the app |
| Add a movie / series / artist | theater-radarr-add... |
input | adds with the default profile and searches (asks first) |
| Wanted movies / episodes / albums | theater-radarr-wanted... |
indexed, 5 min | searches for it now |
| Radarr / Sonarr / Lidarr history | theater-radarr-history... |
indexed, 5 min | opens in the app |
| Downloads | theater-downloads |
live, 20 s | opens the download client |
| Download history | theater-downloads-history |
indexed, 2 min | opens the download client |
| Indexers | theater-prowlarr |
indexed, 5 min | opens Prowlarr's indexers |
| Search indexers | theater-prowlarr-search |
input | grabs through Prowlarr (asks first) |
| Search NZBHydra2 | theater-hydra-search |
input | sends the NZB to SABnzbd (asks first) |
| Navidrome | theater-navidrome |
live, 60 s | opens in Navidrome |
| Search Navidrome | theater-navidrome-search |
input | opens in Navidrome |
| Audiobookshelf | theater-abs |
live, 60 s | opens in Audiobookshelf |
| Search Audiobookshelf | theater-abs-search |
input | opens in Audiobookshelf |
| Kavita | theater-kavita |
live, 60 s | opens the web reader |
| Search Kavita | theater-kavita-search |
input | opens the web reader |
| Request a book | theater-shelfmark |
input | lists the releases for the book |
| Releases | theater-shelfmark-releases |
pushed | downloads the release (asks first) |
| Subtitles | theater-bazarr |
indexed, 5 min | opens in Bazarr |
Theater. Every configured service as a row: the tile, the version and
one key number in the subtitle (Jellyfin: who is watching; Jellyseerr:
pending requests; the arrs: the queue with the stuck count and any health
warning; SABnzbd: the speed, the queue and the free disk; qBittorrent:
active torrents and the connection state; Bazarr: what wants subtitles
and a throttled provider; Prowlarr: indexers and failing ones), a green
up, an amber attention or a red down tag with the error. Enter opens
the web UI, ⌘Enter the service's palette in pal, ⌘K lists its other
palettes as drill-ins, ⌘C copies the URL, ⌘R probes again (the probes
cache 30 s, every one with its own 3.5 s timeout, all in parallel).
Filebrowser and Homepage are open rows.
Jellyfin. Continue Watching (the position as a percentage) then
Latest, the poster as the row's icon (Jellyfin serves images without a
key), an episode led by its series and code; favourites and played items
tagged. The pane: the poster, the overview, year, runtime, rating,
genres, watched state, when it was added. Search lists movies, series,
episodes, albums and songs by section 300 ms after the last key. On every
item: Open (Enter), Play on a device (⌘Enter: a picker of the
sessions that take remote control, Enter plays there), Mark played or
unplayed (⌘⇧P), Favourite (⌘F), Copy link (⌘C). Now Playing lists
the sessions with something on: user, device, client, the position,
paused or playing, transcoding tagged; ⌘Enter pauses or resumes,
⌘⇧S stops (asks). The rows are about one user: jellyfin_user, else
the first administrator.
Jellyseerr. Requests, pending first, the title resolved from TMDB
through the server (cached an hour), the poster, who asked, the status
and 4K tags; the filter narrows to pending, approved, available or all.
On a pending one: Approve (⌘Enter), Decline (⌘⇧D, asks). Request a
title searches TMDB through Jellyseerr (people dropped), a title already
requested or available tagged; Enter requests (asks), ⌘Enter in 4K when
the server enables it for that type, ⌘O opens it, ⌘C copies the TMDB
link.
Radarr, Sonarr, Lidarr (one module, arr.ts). The home palette:
the commands (Add, Wanted, History, Sync watchlist now, Open), the
health warnings from /health, the queue (the movie, episode or album
the record is about, the release, a state tag: downloading, importing,
needs a hand for an import that waits on a manual choice, failed,
error; the size, the percentage, the time left), then the next seven
days from the calendar. On a queue row: Open, Remove from queue (⌘⌫,
asks; removed from the client, never blocklisted), Copy release title.
Sync watchlist runs ImportListSync once per enabled import list by
definitionId, the form that ignores the Trakt list's hardcoded 12 h
refresh interval. Wanted: the newest 40 monitored items without a file
with a line saying how many more; Enter runs the search command for that
one (MoviesSearch, EpisodeSearch, AlbumSearch), ⌘Enter opens it.
History: grabbed, imported, failed, deleted with the release, the quality
and the client's message. Add: a lookup, one already in the library
tagged; Enter adds with the first quality profile, root folder (and
metadata profile on Lidarr), monitored, and starts a search (asks);
⌘Enter adds without searching; ⌘O opens it on TMDB, TVDB or
MusicBrainz.
Downloads. SABnzbd and qBittorrent as one queue, sectioned by
client, the active items first: the name, the category, the speed, the
time left, the size; a state tag (downloading, queued, paused, stalled,
metadata, checking, error) and the percentage. Pause all / Resume all
and Set speed limit on top (a form: a percentage or a rate such as 2M
for SABnzbd, a rate for qBittorrent, 0 lifts it). On a row: Open the
client, Pause or Resume (⌘Enter), Delete with its files (⌘⌫, asks),
Copy name. qBittorrent 5 is driven with stop/start, older ones with
pause/resume; a bearer API key (5.2+) signs the requests, else the
WebUI user and password with a Referer on the login (without one
qBittorrent's CSRF guard reports a wrong password). Download history:
what SABnzbd completed or failed (the reason on the row and in the
pane), the torrents qBittorrent has finished; ⌘⌫ removes one from the
history (the torrent without its files).
Indexers. Prowlarr's indexers by protocol with priority and privacy,
a failing one red with Prowlarr's reason and when it retries, a
disabled one grey, Prowlarr's own health warnings on top; ⌘Enter
runs the indexer's test. Search indexers asks every enabled indexer
(up to 30 s), the rows sectioned by indexer with size, age, category,
flags, seeders or grabs; Enter grabs the release through Prowlarr into
its download client (asks), ⌘Enter opens the release page, ⌘C copies
the magnet or download link. Search NZBHydra2 is the same over Hydra's
newznab API; Enter sends the NZB to SABnzbd when it is set up (asks),
else opens Hydra.
Navidrome (Subsonic API, a salted token per request, the password
never on the wire). What plays now (who, on what player), the albums
added lately with covers; search by artist, album and song; Enter opens
the album or artist in Navidrome's web app, ⌘S stars or unstars, ⌘C
copies the link. Audiobookshelf: continue listening with the
progress, then what was added lately per library; search over books,
podcasts and authors; Mark finished (⌘⇧P, asks). Kavita: On Deck
with the page reached, the series added lately; search over series,
collections, reading lists and files; Enter opens the web reader.
Request a book (Shelfmark): with nothing typed, what Shelfmark is
downloading, queued or failed on; a search on its metadata provider
lists books with the author, year and series; Enter pushes the
releases found for one (format, size, indexer, seeders) and Enter on a
release downloads it into the ebook library (asks) with the same
payload Shelfmark's own button posts. Subtitles (Bazarr): how many
movies and episodes want subtitles (Enter runs Bazarr's wanted-search
tasks, asks), each provider's health with a throttled one's retry, the
wanted items with the missing languages as tags; ⌘Enter searches for
that one.
The bar items, each a { view } popover with a cursor the arrows
move and a click sets, p opening the palette:
theater/downloads: the combined download speed and the active count, amber andpaused · Nwhile everything is paused; hidden while both clients are idle. Every 15 s and on show, wake, network. The popover: the queue with a progress bar per row, Enter opens the client,spacepauses or resumes everything,⌘Enterthe focused item,⌫deletes it (asks). Statesdownloading,speed,paused.theater/playing: who is watching on Jellyfin (the name, or the count); hidden while nothing plays. Every 30 s. The popover: the sessions with the poster and the position,spacepauses or resumes the focused one. Statewatching.theater/requests: pending Jellyseerr requests as the badge, hidden at zero. Every 300 s. The popover: the requests with their posters,aapproves,ddeclines (asks) the focused one. Statepending.theater/queue: the arr queues combined, the stuck count as the badge and amber; hidden while empty. Every 60 s. The popover: each item with its state and progress,⌫removes the focused one (asks). Statesqueued,stuck.
Links. pal://theater/search?q= and pal://theater/request?q=
open Search Jellyfin and Request a title with the query typed;
pal://theater/downloads opens Downloads; pal://theater/pause-all and
resume-all reach both clients; pal://theater/sync-watchlist?app=radarr
(both when omitted) runs the import list sync; pal://theater/open?service=kavita
opens a service's web UI.
Settings, [extensions.theater]: <service>_url for every service
(jellyfin, seerr, radarr, sonarr, lidarr, prowlarr, hydra,
bazarr, sab, qbit, navidrome, abs, kavita, shelfmark,
filebrowser, homepage); <service>_key (secret) for Jellyfin,
Jellyseerr, the arrs, Prowlarr, NZBHydra2, Bazarr and SABnzbd;
qbit_key (secret, a 5.2+ API key) or qbit_user and qbit_password;
<service>_user and <service>_password (secret) for Navidrome,
Audiobookshelf and Kavita; jellyfin_user, whose watched state the rows
show. For the tests, PAL_THEATER_<SERVICE>_<URL|KEY|USER|PASSWORD>
replaces any of them.
DPI Bypass (dpi, dpi-test, dpi/bypass)
The owner's dpi script (~/.local/bin/dpi: byedpi as a SOCKS proxy
with the active network service's DNS and SOCKS pointed at it on macOS,
zapret and dnscrypt-proxy under systemd on Linux) as a toggle with an
indicator, from dpi/. The script is the engine; pal runs
on, off, toggle, status and build and draws what it says. A
live, normal palette: Turn on bypass / Turn off bypass leads it (Enter
runs the script, the panel hides, the HUD says the script's own line,
dpi on (Wi-Fi -> socks5://127.0.0.1:1080, dns 1.1.1.1 9.9.9.9); the row
is in the root's Now section while on), then Status (the facts in the
detail pane: proxy, service, DNS, SOCKS, or the units and the DNS probe;
Enter copies the script's output), Test the bypass, Open the log and
Build byedpi (macOS; the build asks first and the HUD says when it
landed), Copy the status. A partial status (the proxy up but the
service's SOCKS or DNS not pointing at it, or a service still pointed at
a proxy that died; one Linux unit down, or both up with the resolver
still answering the block page) is amber with the reason on the row and
gets a Repair row: off, then on. Without the script, one row saying where
it lives, Enter on the tool setting. on and off take about five
seconds on macOS: waited for up to seven inside the pick, then the HUD
says it is under way and the line lands when the script finishes.
Bypass Test (dpi-test, a view): the script's dpi test run here one
curl per url, all at once, through socks5h://127.0.0.1:<port> while the
proxy is up on macOS, direct otherwise; each row the host and its url, a
blocked or control badge, the code as a badge (green 2xx/3xx, amber
4xx/5xx, red 000) and the time, landing as each curl answers; the
header 2 blocked reach, 4 controls fine, or what failed by host.
Opening the level runs it. test_urls is url or url = blocked|control
(the first two blocked without a role, the rest controls).
| keys | action |
|---|---|
enter |
Turn the bypass on or off; on Status, copy it; in the test, open the url |
cmd+t |
Test the bypass |
cmd+shift+r |
Repair: off, then on (while partial) |
cmd+c |
Copy the status; in the test, the report |
c |
Test: copy the row's line |
cmd+r |
Test: run it again |
up down j k |
Test: move the ring |
pal://dpi/toggle, pal://dpi/on, pal://dpi/off (the HUD says the
script's line), pal://dpi/test (the panel on the test level),
pal://dpi/status (the palette). States dpi/on, dpi/state (off,
on, partial), dpi/service.
The bar item dpi/bypass: a shield while on, hidden while off by its
off rule (show = "always" keeps a muted shield), amber by its
partial rule; refreshed every minute and on show, wake, network.
The popover: the status card with the facts, Enter turns it on or off,
t runs the test with its rows landing under the card, r repairs, c
copies the status, l opens the log (macOS), o opens the palette.
Linux: sudo -n true is probed before a switch; a closed credential
window is a toast saying to run sudo -v in a terminal, and nothing
runs. No log or build rows there.
Settings, [extensions.dpi]:
| key | type | default | what |
|---|---|---|---|
tool |
path | dpi |
The script, a name on PATH or a path; ~/.local/bin is looked in too. |
test_urls |
list | the script's six | What the test curls, url or `url = blocked |
dns |
list | 1.1.1.1, 9.9.9.9 |
The resolvers dpi on sets on macOS, to judge partial; empty leaves DNS unjudged. |
Rendered from docs/palettes.md in the pal repo, 2026-11-02.