Gmail
Your inbox in the panel, its count on the bar, per account
palupdated 2026-09-29IntegrationIn pal's registryMenu bar: Unread
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.






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
enteropens the thread in Gmail,cmd+entermarks 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})
inboxliveThe 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})
searchinputGmail'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 1hEvery 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})
composelistA 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})
draftsliveThe 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_commandA 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.
addressThe account's address, for the links into Gmail and the bar tooltip; read from the API when empty.
labelsLabels 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).
sendOff (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.
signatureAppended 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 inlabels; 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 withsendon (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,mmarks it read,sstars it,amarks every listed message read,oopens Gmail,pthe Inbox palette. Mark several rows (⌘-click,⇧-click for a range,⇧↓) andmmarks 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-platformonce, thengcloud auth application-default print-access-tokenis the command.gmail.readonlyis enough for the rows; mark-read needsgmail.modify, and sendinggmail.sendorgmail.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'"andaud=gmail-work. The remote command needs its own quotes: the remote shell globs the?in the url otherwise, and thatno matches foundis 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.




