Skip to content

Integration for 1.3.0 - #104

Merged
momenbasel merged 23 commits into
mainfrom
integration/1.3.0
Oct 8, 2026
Merged

momenbasel merged 23 commits into
mainfrom
integration/1.3.0

Conversation

@momenbasel

@momenbasel momenbasel commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Integration branch for 1.3.0. Every contributor commit is cherry-picked with its author kept; the follow-up commit after each one is the review fix. The original PRs are closed with a pointer to their landing commit once this is in.

From @alvst, with fixes:

Rewritten here after their PRs:

Also: a promote-on-paste option, off by default, and a one-time toast when direct paste is blocked by a missing Accessibility grant.

Tested: swift build and swift test (7 tests) on this branch.

alvst and others added 23 commits October 8, 2026 04:52
Plays the system "Tink" sound when a clip is copied from the bar via
copyItem(_:), kept audibly distinct from the existing "Play sound on
paste" toggle, which plays "Pop" from PasteService.paste(_:).

Both sound toggles ship disabled: an update should not introduce a new
audible behavior existing users never asked for, and two sibling
settings with opposite defaults would read as an oversight.
"Play sound on copy" read as a sound for every copy Pesty captures. The
only thing that plays it is the card menu's Copy, so the toggle now
reads "Play sound when copying from Pesty".

Based on #90 by @alvst.
The Keep History section gains a "Currently storing" row: clip count
plus the store directory's actual size (history JSON and saved
images), walked off the main actor when the pane opens so a large
image library can't hitch the Settings window.
The size was measured once when the pane appeared, so clearing history
left the old number next to "0 clips" until the window was rebuilt. It
is now re-measured after every store save. The bytes cover history and
pinboards, since pinned clips keep their own image files, so the row is
labeled that way and the clip count includes pinned clips.

Based on #91 by @alvst.
The footer's "Image" and "1 file" told you nothing you couldn't
already see from the card itself. Image clips now read their pixel
dimensions - "1920 × 1080" - and file clips say which files they
actually point at.

What a file clip's footer says depends on what the file is. An image
already shows what it is by sitting on the card, so its dimensions are
the useful fact - a screenshot's path is a timestamped folder nobody
reads. Everything else gets its full location instead, abbreviated
with "~", because two documents of the same name differ only by where
they live. Multi-file clips list every filename rather than a bare
"N files" count, which named none of them. Whether a file is an image
is decided from its path extension, never by opening it: the footer is
evaluated on every render, so it must not touch disk.

A path is worth reading in full, so the meta line wraps to three lines
for file clips rather than collapsing to an ellipsis; every other type
stays on one line. The footer row aligns on its bottom edge so the
quick-paste hint stays put as the path grows.

Dimensions come from the image file's metadata via CGImageSource
rather than from decoding it, and are cached by path: the footer asks
on every render, so a decode per card would be paid over and over for
a number that never changes. Clips whose image file is missing or
unreadable fall back to the old "Image" label, and an image file whose
size can't be read falls back to its filename.
ImagePixelSize cached only successful reads, so a card whose image file
is gone asked ImageIO for it again on every render. Misses are cached
too, and the cache is emptied once it passes 512 entries.

abbreviatingWithTildeInPath goes through NSHomeDirectory, which in the
sandboxed App Store build is the container, so paths never shortened
there. The home now comes from getpwuid, which a sandboxed probe showed
is the only one of NSHomeDirectory, homeDirectoryForCurrentUser and
NSHomeDirectoryForUser(NSUserName()) that returns /Users/<name>.

Based on #95 by @alvst.
Replaces the "remove clips older than" day-choice Picker with a
10-stop slider (1 Day through Forever) matching Alvie's Pesty's
granularity, via a new HistoryRetentionPreset enum. Adds three new
stops baseline didn't have (3 Weeks, 2 Months, Forever) - Forever is
represented as historyRetentionDays == 0, meaning "no automatic
pruning by age", with only the existing safety cap still applying.

Keeps the confirm-before-apply destructive-change dialog baseline
already had; only the presentation of the choice changes, not the
safety UX around it.

The slider itself needed a fix unrelated to its content: on macOS,
Form(.formStyle(.grouped)) lays out every row in a Section's body on
a shared label/control NSGridView, which clamps and right-shifts any
bare control placed there regardless of SwiftUI-side frame modifiers.
Moving the slider block into the Section's footer (plain full-width
content, never part of that grid) fixes it - see the comment at the
call site for how this was confirmed.
The slider wrote the draft at every stop it crossed, and each write ran
the removal check: dragging from 1 Year toward 1 Day committed 6 Months
on the way past it, then put up the confirm alert for 3 Months while
the mouse was still down, and Cancel restored 6 Months instead of 1
Year. The slider now moves a local value and applies it once editing
ends (or on a keyboard step), so nothing is committed during a drag and
Cancel returns to the value the drag started from. The stop under the
thumb is still shown live.

retentionCutoff returns nil for 0 days instead of silently treating it
as 1, so Forever cannot regress through a new caller.

Based on #89 by @alvst.
A "Clip Navigation" section in Settings chooses where the selected clip
parks in the strip: centered, as before, or at the right edge the way
Paste does. The bar's scroll-to-selection reads the preference for its
anchor and re-scrolls when the preference changes while a clip is
selected.
A "Clip Colors" section in Settings offers three palettes for the card
headers: the per-source-app palette cards have always used, a boosted
version of it with higher saturation and contrast, and Accent Shades,
which derives ten deterministic shades from one user-picked base color
(FNV-1a of the bundle identifier picks the shade, so an app keeps its
shade across launches) with a live swatch preview. The boosted palette
is the default here.
The themes commit made the boosted palette the default, which would have
recolored every card on update. Default is again the palette cards have
always used, the boosted treatment is an opt-in theme named Vibrant, and
Accent Shades stays opt-in, so the user-visible default is unchanged.

The HSB conversion behind Vibrant ran per card per render. The twelve
boosted colors are now computed once per palette slot, which is what a
bundle id maps to, and Accent Shades reads the base color's HSB once per
chosen color.

Based on #94 by @alvst.
Image clips, and file clips that point at one image file, draw the
picture edge to edge over a transparency checkerboard instead of a
padded thumbnail. The bitmap no longer comes from NSImage(contentsOf:)
inside the view body: ClipPreviewProvider decodes the stored PNG
downsampled off the main actor, asks Quick Look for image files, and
keeps the results in an NSCache bounded by count and byte cost. The
header of a multi-file clip reads "5 files" rather than "File".

Other file clips show the file's Finder icon, resolved in the card's
task and cached, with a "File not found" caption when stat fails with
ENOENT. The caption is never shown in the sandboxed App Store build,
where access to a copied file does not survive a relaunch and the stat
fails for files that are still there.

The full-bleed treatment, the checkerboard view and the file-count
header come from #96 by @alvst. Fixes #102.
- Add a Copied to Clipboard toast, shown after every successful copy
  (Command-C and the context menu's Copy action both go through copyItem).
- Promote the copied clip to the front of history, since copying an
  existing clip back to the pasteboard is a deliberate re-use of it and
  the clipboard monitor correctly ignores Pesty's own pasteboard writes.
promoteCopiedItem went through addCaptured, which inserts the clip it is
handed whenever history has no same-content twin. Copying a Pinboard
card whose history twin had been deleted or trimmed therefore put the
pinboard's own UUID into history, an identity the two containers are
never meant to share: the App Store sync keys records by UUID and lets
the pinboard record win, so that history copy would never upload, and a
later remote apply of the pinboard record would evict it. With no twin,
history now receives an independent copy, minted the same way
saveToPinboard mints one, with its own UUID and image file.

The "Copied to Clipboard" toast shows only when the pasteboard actually
changed; PasteService.copy leaves it untouched for an image whose file
is missing. Command-C requires exactly Command, so Command-Shift-C and
the other combinations fall through as before.

Based on #82 by @alvst.
Two new context-menu paste modes alongside Paste as Plain Text, enabled
whenever a clip has rich content. Clean Formatting keeps bold, italic,
underline, strikethrough, and links while normalizing fonts, sizes, and
colors to the system default. Markdown converts the rich content to
Markdown text, splitting style markers across line breaks so the output
stays valid, and rendering links as [text](url).

The clipboard monitor now also captures a copy's HTML flavor - browser
copies often carry HTML with no RTF - without changing how clips are
classified. Conversions prefer HTML, then RTF, then fall back to plain
text. Paste plumbing moves from an asPlainText flag to a PasteFormat
enum carrying all four modes.

The HTML importer behind the Clean Formatting and Markdown conversions
is WebKit-backed: handed raw captured HTML, it fetches remote
subresources - tracking pixels included - synchronously on the main
thread at paste time, breaking the no-network-calls promise. Strip
every element that can reference an external resource (img, picture,
source, iframe, object, embed, link, script, style, svg, video, audio,
and CSS url() values) before conversion; the converters only keep text
plus bold/italic/underline/links, so nothing representable is lost. A
2-second importer timeout backs the stripping up, and sources over 1MB
skip rich conversion entirely, falling back to plain text so a
pathological clip can't hitch the paste.

Capture-side, cap stored pasteboard HTML at the sync layer's inline
payload bound (CKSchema.inlineLimit) so every routine text copy can't
persist an unbounded HTML blob; oversized HTML is dropped and the
conversions fall back to RTF/plain text as before.
The regex stripping that ran before the HTML importer rewrote visible
text along with markup: every url(...) in the clip's own words, a
copied CSS snippet or the URL("https://...") call in a code sample,
pasted as "none" through Clean Formatting and Markdown. It also guarded
against something the importer does not do. NSAttributedString(html:)
loads no subresources at all: a local HTTP server logged no request for
an unsanitized img, input type=image, body background, image-set() or
meta refresh. The HTML now reaches the importer as captured; the 2 s
document timeout and the 1 MB conversion cap stay.

Editing a clip replaced its text and RTF but kept the captured HTML,
which the converters prefer, so Paste as Markdown after an edit
produced the pre-edit words. Both edit paths clear it.

HTML is captured only when the pasteboard carries no RTF, the browser
case the conversions need it for, and only up to 64 KB: store.json is
re-encoded and rewritten after every capture, and Safari copies already
carry RTF. The independent copies made for Pinboards and for history
promotions carry the HTML along.

Based on #93 by @alvst.
The previous search UI was a synthetic pill built from captured
keystrokes — appended-only, no cursor, no click-to-edit, no text
selection or IME support. NativeBarSearchField is a real NSTextField
edited through the normal AppKit responder chain (kept mounted even in
its compact zero-width state, so the bar's key monitor can focus it
synchronously and hand off the very first keystroke without losing it).

BarInputMode tracks whether focus belongs to the field or the clip
strip, so the monitor and the field agree on who owns a given key
without probing the first-responder chain from every call site.
The field took every key while it had focus. Return resigned it and
handed focus back to the cards instead of pasting, so the open, type,
Return flow needed two Returns; Command-1 to Command-9 went to the
field editor, which has no binding for them; and Up, Down, Left and
Right moved a caret inside a one-line query instead of the selection.
Return now pastes the selected clip from the field's delegate, the
quick-paste digits are handled before the field takes the event, and
the delegate routes Up and Down, plus Left and Right once the caret
sits at the matching end of the query, to the strip's selection.

A hide while the field was focused (Escape on an empty query, the
hotkey, a click outside) left it first responder of the ordered-out
panel, so the next presentation came up in search mode: pill expanded,
first Return swallowed, arrows moving the caret. showBar resigns the
field and resets the input mode before anything else.

Two comments described another project's version of this file and are
gone.

Based on #85 by @alvst.
- BarResizeGeometry centralizes bar-height sizing rules (default/min/max,
  per-display clamping, and the macOS 26 glass content frame math) shared
  by both the persisted preference and the live drag session.
- BarResizeHandle is a compact AppKit drag target (with accessibility
  increment/decrement support) shown in an optional overlay at the top
  of the bar.
- A new "Show resize handle on the Paste Bar" Settings toggle gates it,
  off by default. The bar's own key monitor steps aside while the handle
  owns first responder, so Escape/arrows during a drag reach it directly.

The height slider in Settings previews the same size. Clicking into
Settings dismisses the bar, so a pixel count was previously the only
feedback for a dimension that is only meaningful on screen. Dragging the
slider now live-resizes the real bar when it happens to be up, and
otherwise raises BarHeightGhostController: a translucent, click-through
panel occupying exactly the frame the bar would take on the display under
the pointer, drawing a faded real BarView behind a dashed outline and a
large "N px" readout, and retiring itself about 1.1s after the last
change. Showing the bar hides any outline still lingering.

The bar-height slider's unit label reads px instead of pt.
The Settings slider previewed its value with a second panel: a
click-through window at modal-panel level carrying a faded live BarView,
a dashed outline and a readout, raised on every slider stop. On a laptop
display it covers the Settings window it is meant to serve, and it was
the one part of the change the new toggle did not gate. The panel, the
slider wiring and the live resize it drove are gone; the handle on the
bar stays, off by default behind its toggle.

The slider label reads pt again: the value is in points (430 pt is 860
px on a Retina display), which the first of the two commits had right.

After a drag the handle restored the previous first responder and kept
the keys when that failed, leaving the bar's key monitor stepping aside
for good; the window itself now takes first responder when the restore
is refused. Escape is named by its Carbon constant.

Based on #87 by @alvst.
A deleted clip, or every clip of a bulk delete together, can be put
back with Command-Z for five minutes, at the position it came from:
after its former predecessor if that clip is still there, else before
its successor, else at its old index. An Undo button sits in the bar's
top right while anything is restorable, the bulk-delete alert says so
instead of "There is no undo", and a clip that came back by other means
in the meantime, copied again or synced in, is left alone rather than
doubled.

The stack lives in memory only. Deleted content is never written back
to store.json, there are no tombstones, and the iCloud Drive merge, the
CloudKit sync and remote applies are untouched. Image files of
restorable clips stay on disk and are removed when the window closes,
on Clear History, when a Pinboard is deleted, or at quit, the four
moments the stack empties. A "Delete permanently" setting, off by
default, skips the stack, and holding Option while pressing Backspace,
Delete or Command-Backspace skips it for that one deletion; the Option
state comes from the key event itself, and the context menu has no
hidden modifier.

Designed after #88 by @alvst, whose placement bookkeeping, button and
alert copy this keeps.
Pasting can move the clip to the front of history the way copying it
from the bar does, behind a "Promote pasted clips to the top of
history" toggle that is off by default: history has always been
ordered by when a clip was copied, and the quick-paste numbers would
shift on every paste. The promotion uses the copy path's guards, so it
happens only when the pasteboard actually changed and never puts a
Pinboard copy's UUID into history.

In the direct-download build a paste that the missing Accessibility
grant turns into a plain copy used to end in silence, which reads as a
broken paste. The toast now says "Copied. Grant Accessibility in System
Settings to paste directly." once per launch, without a modal and
without taking focus, and sizes itself to its text. The App Store build
never reaches that path and compiles none of it.

Both come from #92 by @alvst, whose Command-Delete change and modal
alert are left out.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 02:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@momenbasel
momenbasel merged commit 38f4f25 into main Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants