Gmail

Your inbox in the panel, its count on the bar, per account

palupdated 2026-09-29IntegrationIn pal's registry󱔓Menu bar: Unread󰀐Several accounts

Open in pal

In pal's registry: the button shows it in pal, which asks before installing; the command does the same from a terminal. This page can't see what your pal has installed.

Gmail: Inbox: unread first, the sender, the subject and the snippet, a star or a paperclip, the time
Inbox: unread first, the sender, the subject and the snippet, a star or a paperclip, the time
Gmail: The pane: the message as text with the quote folded, who and when, the labels, the attachment and its size
The pane: the message as text with the quote folded, who and when, the labels, the attachment and its size
Gmail: Search Mail: Gmail's own syntax typed into the panel, the hits sectioned by label
Search Mail: Gmail's own syntax typed into the panel, the hits sectioned by label
Gmail: Labels: yours and Gmail's, Enter opens one in Gmail, cmd+Enter searches it here
Labels: yours and Gmail's, Enter opens one in Gmail, cmd+Enter searches it here
Gmail: Compose: to, cc, subject and body, the signature already under it; only with send on
Compose: to, cc, subject and body, the signature already under it; only with send on
Gmail: Drafts: the recipient and the subject; send or discard, each after asking
Drafts: the recipient and the subject; send or discard, each after asking
1 of 6

What it does

Your Gmail inbox in the panel: read, search with Gmail's own syntax, browse labels and mark messages read, with the unread count on the bar. Each account is its own instance, and sending stays off until you turn it on.

  • The inbox, unread first: sender, subject, snippet, labels and attachments at a glance
  • enter opens the thread in Gmail, cmd+enter marks it read; the side pane shows the message as text
  • Search with Gmail's own syntax (from:, has:attachment, newer_than:7d) and browse every label
  • An unread count on the bar, hidden at zero; its popover lists the unread messages
  • Several accounts side by side, each with its own tile badge, token and bar item
  • Compose, reply, drafts, archive and star, only on accounts where you turn sending on

Palettes 5

  • Inbox ({instance})inboxlive

    The inbox, unread first then recent, with the sender's avatar, the subject, the snippet, the labels, an attachment mark and the date; the pane is the message as text with the quoted replies folded.

    • ↵Open the message in Gmail
    • ⌘↵Mark as read (Mark as unread on a read row)
    • ⌘EArchive (send on)
    • ⌘SStar or unstar (send on)
    • ⌘⇧RReply with the text typed in the bar (send on)
    • ⌘CCopy the link
  • Search Mail ({instance})searchinput

    Gmail's search as you type, its syntax passed through (from:, to:, subject:, has:attachment, newer_than:7d, label:), sectioned by label.

    • ↵Open the message in Gmail
    • ⌘↵Mark as read (Mark as unread on a read row)
    • ⌘EArchive (send on)
    • ⌘SStar or unstar (send on)
    • ⌘⇧RReply with the text typed in the bar (send on)
    • ⌘CCopy the link
  • Labels ({instance})labelslistcatalogrefreshes every 1h

    Every label of the account, yours and Gmail's; a row opens the label in Gmail or searches it here.

    • ↵Open the label in Gmail
    • ⌘↵Search the label here
    • ⌘CCopy the label name
  • Compose ({instance})composelist

    A new message as a form: to, cc, subject, body; the signature under it. Only with send on for the account.

    • ↵Open the form; on the form, send
  • Drafts ({instance})draftslive

    The drafts of the account with the recipient and the subject; open one in Gmail, send it, or discard it. Only with send on for the account.

    • ↵Open the draft in Gmail
    • ⌘↵Send the draft, after a confirmation
    • ⌘DDiscard the draft, after a confirmation

Actions

  • Open in GmailThe thread in the browser, in the account's own Gmail.↵
  • Mark as readOn an unread row; Mark as unread on a read one. Mark several rows (cmd-click, shift-click) to do them at once.⌘↵
  • ArchiveOut of the inbox; send on only.⌘E
  • StarStar or unstar; send on only.⌘S
  • ReplyThe text typed in the bar goes to the sender under the Re: subject, the original quoted under it; a form with to, cc, subject and body when picked without it; send on only.⌘⇧R
  • Copy link⌘C
  • Search labelOn a label row: the search palette with label:<name> typed.⌘↵
  • Copy nameOn a label row.⌘C
  • ComposeThe form, then the message from the account.↵
  • Send draftAsks first.⌘↵
  • Discard draftAsks first.⌘D
  • Open Gmail settingsOn a hint row: the extension's settings.

Settings

Extension, [extensions.gmail]

token_command
empty text

A shell command that prints an access token for the Gmail API: a bare token, or the JSON an OAuth endpoint answers (access_token, expires_in). The command owns the secret; pal keeps the token in memory until it expires and never writes it anywhere. gcloud auth application-default print-access-token, a helper, a broker behind ssh.

address
empty text

The account's address, for the links into Gmail and the bar tooltip; read from the API when empty.

labels
none list

Labels whose unread mail the Inbox palette lists besides the inbox, one per line by name (a filter that skips the inbox, a list you follow).

send
Off boolean

Off (the default) leaves the account read and mark-read only: no compose, no reply, no drafts, no archive, no star, whatever the token could do. Never inherited by another instance.

signature
empty text

Appended under the body of a message composed or replied here, after a blank line.

Defaults shown. Change them in pal's settings window or in the config file.

The README

Everything Gmail does, key by key, from the extension's own README.md

Gmail

Your inbox in the panel and its count on the bar, per account. Five palettes and a bar item over the Gmail API, one instance per account:

  • Inbox (gmail-inbox): unread first, then the newest fifty, then the unread of any label named in labels; each row the sender's avatar, the subject, the sender and the snippet, the user labels as chips, a paperclip when there is an attachment, the date. The pane shows the message as text with the quoted replies folded, the headers and the attachments.
  • Search Mail (gmail-search): Gmail's own search as you type (from:, to:, subject:, has:attachment, newer_than:7d, label:), the hits sectioned by label. The hits show as soon as their headers are in, with the sender's initial; the Gravatars follow.
  • Labels (gmail-labels): yours, then Gmail's and the categories; Enter opens the label in Gmail, cmd+Enter searches it here.
  • Compose (gmail-compose) and Drafts (gmail-drafts): only for an account with send on (below).
  • Unread (gmail/unread, the bar item): the inbox's unread count as a badge, hidden at zero, the account's title beside the glyph; every two minutes and on show, wake and network. The popover is a view of its own: every unread as a row (the sender's mark, who wrote it, the subject and its snippet, the time, a star or a paperclip), with a cursor the arrows move and a click sets. Enter opens the focused message in Gmail, m marks it read, s stars it, a marks every listed message read, o opens Gmail, p the Inbox palette. Mark several rows (⌘-click, ⇧-click for a range, ⇧↓) and m marks them all read at once.

Two accounts

The extension is multi: a second account is a second instance with its own token command, address, palettes, bar item and storage (docs/design/instances.md). The default instance is [extensions.gmail]; another is [extensions."gmail@work"] next to [instances."gmail@work"]:

[extensions.gmail]
token_command = "gcloud auth application-default print-access-token"
send = true
signature = "Cagdas"

[instances.gmail]
title = "Personal"

[instances."gmail@work"]
title = "Work"
tint = "amber"

[extensions."gmail@work"]
token_command = "ssh archer \"curl -s 'http://127.0.0.1:8776/token?aud=gmail-work'\""
address = "someone@example.org"
# send stays false: read and mark-read only

token_command, address and send are scope: instance: never inherited from the default instance, so a second account can never send because the first one may. labels and signature inherit until set. Palette titles carry the instance's title ("Inbox (Work)"), the tile its tint and badge, and the bar item's strip text is the title, so the two accounts read apart everywhere.

The token command

pal has no OAuth flow and ships no Google client id. The account's access token comes from a command of yours, token_command, run through sh -c; its stdout is the token, a bare line or the JSON an OAuth endpoint answers (access_token, expires_in). The token is kept in memory until the expiry it stated (30 minutes for a bare one), minted again once on a 401, and never written anywhere. The command owns the secret; pal only ever holds a short-lived access token.

  • gcloud (the generic path, documented, not tested here): gcloud auth application-default login --scopes=https://www.googleapis.com/auth/gmail.modify,https://www.googleapis.com/auth/cloud-platform once, then gcloud auth application-default print-access-token is the command. gmail.readonly is enough for the rows; mark-read needs gmail.modify, and sending gmail.send or gmail.modify. If Google answers 403 asking for a quota project, gcloud auth application-default set-quota-project <project> with the Gmail API enabled on it.
  • A helper or a broker: anything that prints a token. The owner's accounts go through a broker on another box that mints one per audience: ssh archer "curl -s 'http://127.0.0.1:8776/token?aud=gmail'" and aud=gmail-work. The remote command needs its own quotes: the remote shell globs the ? in the url otherwise, and that no matches found is what the hint row shows.

A command that fails is one hint row in every palette with the command's last stderr line ("Token command exited 7: curl: (7) Failed to connect ...") and Open Gmail settings as its action; an empty command names the fix; a token Gmail rejects says to check the scopes. The bar item hides while there is no token and goes stale on any other failure.

Read and mark-read only, unless you say so

send is off by default, and off means the account is read and mark-read only: no compose, no reply, no drafts, no archive, no star, whatever the token could do. The Inbox and Search rows then carry Open in Gmail, Mark as read (or unread) and Copy link; Compose and Drafts list nothing.

That is the rule for a work account, and the reason send is per instance. Mail leaving an employer's domain commits the employer and is seen by colleagues; that is not a launcher's to originate. The owner's work identity is granted the full Gmail scope because mark-read needs gmail.modify and no Google scope grants mark-read without also granting send, so the rule lives in this setting rather than in the scope: the token can send, the extension does not. Marking read is the one write that stays on, because it is the one the owner uses daily and the one the account's policy allows. Draft the answer and hand it over; send it from Gmail yourself.

With send on for an account (a personal one), Compose is a form (to, cc, subject, body; the signature under the body), Reply on a row takes the answer as the row's typed argument (Tab into the Quick reply field in the bar, ⌘⇧R sends it to the sender under the Re: subject, the original quoted under it, in its thread; Enter still opens the thread; a pick without the text, from pal run or a hotkey, is the full form with to, cc, subject and body), Drafts lists Gmail's drafts to send or discard after a confirmation, Archive and Star join the row's actions.

Rows and actions

action shortcut when
Open in Gmail Enter https://mail.google.com/mail/?authuser=<address>#inbox/<threadId> (#all/ off the inbox; Gmail redirects to the account's /u/N/ slot with the fragment kept)
Mark as read / Mark as unread ⌘Enter messages.batchModify; works over marked rows too, a mix of read and unread included
Archive ⌘E send on; out of the inbox; marked rows too
Star / Unstar ⌘S send on; marked rows too, both offered when they mix starred and not
Reply ⌘⇧R send on; the text typed in the bar's field (the sender and the Re: subject implied) or, without it, the form; then messages.send in the thread
Copy link ⌘C marked rows too, one link per line
Search label ⌘Enter on a label row: Search Mail with label:<name> typed
Send draft / Discard draft ⌘Enter / ⌘D send on; each asks first; Discard (and Open) take marked drafts too, Send one at a time

Marked rows (⌘-click, ⇧-click, ⇧↓) take Open (each thread in the browser), the marks, Archive, the star and Copy link at once; Reply stays one message. In the popover Enter opens every marked message, m marks them read, s stars them all (or, when every one is starred, unstars them, as Gmail's own button does) and ⌘C copies their links.

The letter keys the design asked for (e, s) would type into the search box in a list palette (docs/keyboard.md), so they are ⌘E and ⌘S.

The sender's mark is the Gravatar for the address when there is one (a HEAD per address, remembered for the process), else the initial on a tile tinted from the address.

Requests and quota

messages.list with maxResults 50 (the unread of the inbox, the inbox, each labels entry's unread), then messages.get with format=metadata and the row's headers only, eight at a time, cached by id for the process (a message's headers never change; its labels are patched here on every write); format=full only for the message the pane or a reply opens (the last twenty kept). The unread count is the list's length under a page, labels.get INBOX past it. The label table is read once an hour and persisted in storage, so a restart lists with no call. The inbox is shared between the bar item and the palette for 30 s, so the panel showing (which fires both) costs one read. A 429 (or a 403 naming the quota) is remembered for its Retry-After (60 s without one) and every call until then refused locally.

Settings

key type default what
token_command text, per instance (none) The command that prints the access token.
address text, per instance (none) The account's address 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, per instance false Compose, reply, drafts, archive, star. Off: read and mark-read only.
signature text (none) Under the body of a composed or replied message.

The bar item is hidden at zero; show = "always" under [bar.items."gmail/unread"] keeps the glyph (and the account's title) on the strip anyway, muted, with the same popover (docs/config.md).

For the tests, PAL_GMAIL_API replaces the API host and PAL_GMAIL_AVATARS the Gravatar host (empty turns the probe off).

Not done

The attachment mark on a row is a guess from the top-level MIME type (multipart/mixed) since format=metadata sends no parts; the pane confirms it. Threads are not grouped: a row is a message, opened at its thread. No trash, no labelling from here, no attachment download.