From d17c65b05ae91ec01e5f584a4760d7a67ce07fe0 Mon Sep 17 00:00:00 2001 From: Jacob Mather Date: Mon, 10 Aug 2026 15:05:00 -0400 Subject: [PATCH 1/4] Expose atomic multi-format clipboard writes --- src/lib.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 667152c..8eda540 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ #![deny(clippy::all)] use base64::{engine::general_purpose, Engine as _}; use clipboard_rs::{ - common::RustImage, Clipboard, ClipboardContext, ClipboardHandler, ClipboardWatcher, - ClipboardWatcherContext, ContentFormat, RustImageData, WatcherShutdown, + common::RustImage, Clipboard, ClipboardContent, ClipboardContext, ClipboardHandler, + ClipboardWatcher, ClipboardWatcherContext, ContentFormat, RustImageData, WatcherShutdown, }; use napi::bindgen_prelude::*; use napi::threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunctionCallMode}; @@ -139,6 +139,53 @@ pub async fn set_buffer(format: String, buffer: Vec) -> Result<()> { ctx.set_buffer(&format, buffer).map_err(cb_err) } +#[napi(object)] +pub struct ClipboardBufferContent { + pub format: String, + pub buffer: Vec, +} + +#[napi(object)] +pub struct ClipboardContents { + pub text: Option, + pub html: Option, + pub rtf: Option, + pub buffers: Option>, +} + +/// Write multiple representations of the same clipboard content in a single operation. +#[napi] +pub async fn set_contents(contents: ClipboardContents) -> Result<()> { + let ctx = ClipboardContext::new().map_err(cb_err)?; + let mut clipboard_contents = Vec::new(); + + if let Some(text) = contents.text { + clipboard_contents.push(ClipboardContent::Text(text)); + } + if let Some(html) = contents.html { + clipboard_contents.push(ClipboardContent::Html(html)); + } + if let Some(rtf) = contents.rtf { + clipboard_contents.push(ClipboardContent::Rtf(rtf)); + } + if let Some(buffers) = contents.buffers { + clipboard_contents.extend( + buffers + .into_iter() + .map(|entry| ClipboardContent::Other(entry.format, entry.buffer)), + ); + } + + if clipboard_contents.is_empty() { + return Err(Error::new( + Status::InvalidArg, + "contents is empty; use clear() to clear the clipboard", + )); + } + + ctx.set(clipboard_contents).map_err(cb_err) +} + #[napi] pub async fn clear() -> Result<()> { let ctx = ClipboardContext::new().map_err(cb_err)?; From 21cfe14cae8073968ea0e44ce28c7837f527d438 Mon Sep 17 00:00:00 2001 From: Jacob Mather Date: Mon, 10 Aug 2026 15:05:18 -0400 Subject: [PATCH 2/4] Test atomic multi-format clipboard writes --- __test__/index.spec.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 3148405..56f951b 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -11,6 +11,7 @@ import { hasRtf, hasText, setBuffer, + setContents, setHtml, setText, startWatch, @@ -68,6 +69,30 @@ test.serial('setBuffer / getBuffer round-trips with custom format', async (t) => t.deepEqual(Array.from(result), data) }) +// ── Multiple formats ───────────────────────────────────────────────── + +test.serial('setContents writes multiple clipboard representations at once', async (t) => { + const format = 'com.ava.test-set-contents' + const data = [0x48, 0x65, 0x6c, 0x6c, 0x6f] + + await setContents({ + text: 'hello', + html: '

hello

', + rtf: '{\\rtf1\\ansi hello}', + buffers: [{ format, buffer: data }], + }) + + t.is(await getText(), 'hello') + t.true(hasHtml()) + t.true((await getHtml()).includes('hello')) + t.true(hasRtf()) + t.deepEqual(Array.from(await getBuffer(format)), data) +}) + +test.serial('setContents rejects empty content', async (t) => { + await t.throwsAsync(() => setContents({}), { message: /contents is empty/ }) +}) + // ── Clear ──────────────────────────────────────────────────────────── test.serial('clear removes clipboard content', async (t) => { From d01a5a3478ce4f9b46980a00ed9caf3e31524ea0 Mon Sep 17 00:00:00 2001 From: Jacob Mather Date: Mon, 10 Aug 2026 15:05:29 -0400 Subject: [PATCH 3/4] Update generated TypeScript declarations --- index.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/index.d.ts b/index.d.ts index c514fa1..4ab4e1a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,17 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +export interface ClipboardBufferContent { + format: string + buffer: Array +} + +export interface ClipboardContents { + text?: string + html?: string + rtf?: string + buffers?: Array +} + export declare class ClipboardWatcherJs { stop(): void get isRunning(): boolean @@ -35,6 +47,9 @@ export declare function hasText(): boolean export declare function setBuffer(format: string, buffer: Array): Promise +/** Write multiple representations of the same clipboard content in a single operation. */ +export declare function setContents(contents: ClipboardContents): Promise + export declare function setFiles(files: Array): Promise export declare function setHtml(html: string): Promise From ee400f5426ff8575a244ce411a2e5c6c35dfe93d Mon Sep 17 00:00:00 2001 From: Jacob Mather Date: Mon, 10 Aug 2026 15:56:43 -0400 Subject: [PATCH 4/4] Update generated index files. --- index.d.ts | 18 +++++++++--------- index.js | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4ab4e1a..06e52d0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,14 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +export declare class ClipboardWatcherJs { + stop(): void + get isRunning(): boolean +} + +export declare function availableFormats(): Array + +export declare function clear(): Promise + export interface ClipboardBufferContent { format: string buffer: Array @@ -12,15 +21,6 @@ export interface ClipboardContents { buffers?: Array } -export declare class ClipboardWatcherJs { - stop(): void - get isRunning(): boolean -} - -export declare function availableFormats(): Array - -export declare function clear(): Promise - export declare function getBuffer(format: string): Promise> export declare function getFiles(): Promise> diff --git a/index.js b/index.js index fd95a3a..0cba697 100644 --- a/index.js +++ b/index.js @@ -592,6 +592,7 @@ module.exports.hasImage = nativeBinding.hasImage module.exports.hasRtf = nativeBinding.hasRtf module.exports.hasText = nativeBinding.hasText module.exports.setBuffer = nativeBinding.setBuffer +module.exports.setContents = nativeBinding.setContents module.exports.setFiles = nativeBinding.setFiles module.exports.setHtml = nativeBinding.setHtml module.exports.setImageBase64 = nativeBinding.setImageBase64