Commit ba62721654
ba6272165474d294fe74fc0966638c329e972346
parent: 84a94244b1
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-18 01:22 UTC
Clear the clipboard instead of restoring it
Layout: unified · split
Sources/keycask/Clipboard.swift
+4 −5
| @@ -4,7 +4,6 @@ import KeycaskCore |
| 4 | enum Clipboard { |
4 | enum Clipboard { |
| 5 | struct Handoff: Codable, Equatable { |
5 | struct Handoff: Codable, Equatable { |
| 6 | var secret: String |
6 | var secret: String |
| 7 | var previous: String |
| |
| 8 | } |
7 | } |
| 9 | |
8 | |
| 10 | struct Tool: Equatable { |
9 | struct Tool: Equatable { |
| @@ -14,7 +13,7 @@ enum Clipboard { |
| 14 | |
13 | |
| 15 | static let timeoutSeconds = 45 |
14 | static let timeoutSeconds = 45 |
| 16 | |
15 | |
| 17 | static func shouldRestore(secret: String, current: String?) -> Bool { |
16 | static func shouldClear(secret: String, current: String?) -> Bool { |
| 18 | current == secret |
17 | current == secret |
| 19 | } |
18 | } |
| 20 | |
19 | |
| @@ -75,7 +74,7 @@ enum Clipboard { |
| 75 | |
74 | |
| 76 | static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws { |
75 | static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws { |
| 77 | _ = try requireTool() |
76 | _ = try requireTool() |
| 78 | let handoff = Handoff(secret: secret, previous: try read()) |
77 | let handoff = Handoff(secret: secret) |
| 79 | try write(secret) |
78 | try write(secret) |
| 80 | let process = Process() |
79 | let process = Process() |
| 81 | process.executableURL = Bundle.main.executableURL |
80 | process.executableURL = Bundle.main.executableURL |
| @@ -103,8 +102,8 @@ enum Clipboard { |
| 103 | } |
102 | } |
| 104 | Thread.sleep(forTimeInterval: TimeInterval(seconds)) |
103 | Thread.sleep(forTimeInterval: TimeInterval(seconds)) |
| 105 | let current = try? read() |
104 | let current = try? read() |
| 106 | guard shouldRestore(secret: handoff.secret, current: current) else { return } |
105 | guard shouldClear(secret: handoff.secret, current: current) else { return } |
| 107 | try write(handoff.previous) |
106 | try write("") |
| 108 | } |
107 | } |
| 109 | |
108 | |
| 110 | private static func requireTool() throws -> Tool { |
109 | private static func requireTool() throws -> Tool { |
Tests/KeycaskCLITests/ClipboardTests.swift
+5 −5
| @@ -4,14 +4,14 @@ import Testing |
| 4 | @testable import keycask |
4 | @testable import keycask |
| 5 | |
5 | |
| 6 | @Suite struct ClipboardTests { |
6 | @Suite struct ClipboardTests { |
| 7 | @Test func restoresOnlyWhenClipboardStillHoldsTheSecret() { |
7 | @Test func clearsOnlyWhenClipboardStillHoldsTheSecret() { |
| 8 | #expect(Clipboard.shouldRestore(secret: "s", current: "s")) |
8 | #expect(Clipboard.shouldClear(secret: "s", current: "s")) |
| 9 | #expect(!Clipboard.shouldRestore(secret: "s", current: "user pasted")) |
9 | #expect(!Clipboard.shouldClear(secret: "s", current: "user pasted")) |
| 10 | #expect(!Clipboard.shouldRestore(secret: "s", current: nil)) |
10 | #expect(!Clipboard.shouldClear(secret: "s", current: nil)) |
| 11 | } |
11 | } |
| 12 | |
12 | |
| 13 | @Test func handoffRoundTrips() throws { |
13 | @Test func handoffRoundTrips() throws { |
| 14 | let h = Clipboard.Handoff(secret: "s3cret", previous: "old") |
14 | let h = Clipboard.Handoff(secret: "s3cret") |
| 15 | let data = try JSONEncoder().encode(h) |
15 | let data = try JSONEncoder().encode(h) |
| 16 | #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h) |
16 | #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h) |
| 17 | } |
17 | } |
docs/superpowers/specs/2026-09-17-keycask-design.md
+7 −6
| @@ -224,11 +224,12 @@ and a Windows body where they differ. |
| 224 | Linux; `clip.exe` to write and `powershell -command Get-Clipboard` to |
224 | Linux; `clip.exe` to write and `powershell -command Get-Clipboard` to |
| 225 | read on Windows. No tool found is exit 1 with a message naming the |
225 | read on Windows. No tool found is exit 1 with a message naming the |
| 226 | tools. `clip` spawns `keycask clipboard-daemon` detached with stdout |
226 | tools. `clip` spawns `keycask clipboard-daemon` detached with stdout |
| 227 | and stderr to null, writes `{"secret": ..., "previous": ...}` to its |
227 | and stderr to null and exits without waiting. `clip` writes the |
| 228 | stdin, and exits without waiting. `clip` writes the clipboard itself, |
228 | clipboard itself, then spawns the daemon with `{"secret": ...}` on its |
| 229 | then spawns the daemon. The daemon sleeps 45 seconds, reads the |
229 | stdin. The daemon sleeps 45 seconds, reads the clipboard, and clears |
| 230 | clipboard, and if it still equals the secret restores `previous` or |
230 | it if it still equals the secret. It never restores earlier contents, |
| 231 | clears when `previous` is empty. |
231 | so a second `clip` inside the window cannot bring an earlier secret |
| |
232 | back. |
| 232 | |
233 | |
| 233 | ## Errors |
234 | ## Errors |
| 234 | |
235 | |
| @@ -272,7 +273,7 @@ CLI, black box: |
| 272 | masking versus `--reveal`, `--field` raw output, the ambiguous-name |
273 | masking versus `--reveal`, `--field` raw output, the ambiguous-name |
| 273 | listing, `rm` without `--yes` and without a TTY, `edit` with no flags, |
274 | listing, `rm` without `--yes` and without a TTY, `edit` with no flags, |
| 274 | `--generate` with `--words`, and `init` on an existing vault. |
275 | `--generate` with `--words`, and `init` on an existing vault. |
| 275 | - The clipboard daemon's restore decision is unit-tested in process; |
276 | - The clipboard daemon's clear decision is unit-tested in process; |
| 276 | the tests do not touch the real clipboard. |
277 | the tests do not touch the real clipboard. |
| 277 | |
278 | |
| 278 | The CLI suite is the conformance suite. When the app exists, its |
279 | The CLI suite is the conformance suite. When the app exists, its |