From 17d513c62fd1a15e909337de280fb69993783ef6 Mon Sep 17 00:00:00 2001 From: jess Date: Fri, 17 Apr 2026 13:07:08 -0700 Subject: [PATCH] Clipboard handling needed revision in preperation for Windows Support. --- viewport/Cargo.toml | 1 + viewport/src/editor.rs | 26 ++++--------- viewport/src/handle.rs | 87 ++++++++++++++++++++++++------------------ 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/viewport/Cargo.toml b/viewport/Cargo.toml index 81553ae..e5077b5 100644 --- a/viewport/Cargo.toml +++ b/viewport/Cargo.toml @@ -22,6 +22,7 @@ serde_json = "1" toml = "0.8" zip = { version = "2", default-features = false, features = ["deflate"] } base64 = "0.22" +arboard = "3" [build-dependencies] cbindgen = "0.29" diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index 71ecabc..d618aeb 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -165,21 +165,6 @@ pub struct InlinePressState { const LONG_PRESS_MS: u128 = 300; -/// Write `s` to the macOS system clipboard via `pbcopy`. Mirrors the -/// implementation in `handle.rs::MacClipboard::write` so the editor can copy -/// without threading a clipboard handle through update(). -fn pbcopy(s: &str) { - use std::io::Write; - if let Ok(mut child) = std::process::Command::new("pbcopy") - .stdin(std::process::Stdio::piped()) - .spawn() - { - if let Some(stdin) = child.stdin.as_mut() { - let _ = stdin.write_all(s.as_bytes()); - } - let _ = child.wait(); - } -} pub const ERROR_PREFIX: &str = "⚠ "; const EVAL_DEBOUNCE_MS: u128 = 300; @@ -392,6 +377,11 @@ pub struct EditorState { /// Whether the gutter line numbers cycle through the rainbow palette /// based on distance from the cursor. Independent of `line_indicator`. pub gutter_rainbow: bool, + + /// Cross-platform clipboard out-channel. Editor logic writes here; + /// the shell drains it after each frame via `viewport_take_clipboard` + /// and pushes the text to the system clipboard. + pub pending_clipboard: Option, } /// Per-eval table name→id bookkeeping. `keys` is every alias a table is @@ -504,6 +494,7 @@ impl EditorState { inline_press: None, line_indicator: LineIndicator::On, gutter_rainbow: true, + pending_clipboard: None, } } @@ -3046,15 +3037,14 @@ impl EditorState { /// Copy `{line} → {value}` to clipboard. Used by both long-press (just /// copy) and double-click (copy then insert template). - fn copy_inline_result(&self, block_id: crate::selection::BlockId, after_line: usize) { + fn copy_inline_result(&mut self, block_id: crate::selection::BlockId, after_line: usize) { let value = match self.inline_result_value(block_id, after_line) { Some(v) => v, None => return, }; let line = self.read_line_at(block_id, after_line).unwrap_or_default(); let trimmed = line.trim_end(); - let clip = format!("{trimmed} {RESULT_PREFIX}{value}"); - pbcopy(&clip); + self.pending_clipboard = Some(format!("{trimmed} {RESULT_PREFIX}{value}")); } /// Double-click on a result: copy + drop a `let = value` line two lines diff --git a/viewport/src/handle.rs b/viewport/src/handle.rs index b6814e2..b77a656 100644 --- a/viewport/src/handle.rs +++ b/viewport/src/handle.rs @@ -7,67 +7,71 @@ use iced_wgpu::core::renderer::Style; use iced_wgpu::core::time::Instant; use iced_wgpu::core::{clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme}; use iced_wgpu::Engine; -use raw_window_handle::{ - AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle, -}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; +#[cfg(target_os = "macos")] +use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle}; +#[cfg(target_os = "windows")] +use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle}; use crate::editor::{EditorState, Message, RenderMode}; use crate::palette; use crate::table_block::TableMessage; use crate::ViewportHandle; -struct MacClipboard; +struct AcordClipboard { + board: std::cell::RefCell, +} -impl clipboard::Clipboard for MacClipboard { +impl clipboard::Clipboard for AcordClipboard { fn read(&self, _kind: clipboard::Kind) -> Option { - // `from_utf8_lossy` rather than strict `from_utf8` — some rich-text - // clipboard sources produce stray bytes when pbpaste coerces their - // format to plain text, and a single bad byte would otherwise drop - // the whole paste. - // - // Line-ending normalisation: web pages, Discord, and some cross- - // platform apps keep `\r\n` in the pasteboard. Iced's buffer and - // our own gutter line counter disagree about whether `\r` is its - // own row, which drifts the gutter against the cursor on every - // paste. Collapse every CR to LF before handing the text upward. - std::process::Command::new("pbpaste") - .output() + // arboard uses NSPasteboard on macOS, Win32 on Windows — no subprocess. + // Line-ending normalisation: web pages and cross-platform apps keep + // `\r\n` in the pasteboard; collapse to `\n` so iced's buffer and + // our gutter line counter agree. + self.board.borrow_mut() + .get_text() .ok() - .map(|o| { - let s = String::from_utf8_lossy(&o.stdout).into_owned(); - s.replace("\r\n", "\n").replace('\r', "\n") - }) + .map(|s| s.replace("\r\n", "\n").replace('\r', "\n")) } fn write(&mut self, _kind: clipboard::Kind, contents: String) { - use std::io::Write; - if let Ok(mut child) = std::process::Command::new("pbcopy") - .stdin(std::process::Stdio::piped()) - .spawn() - { - if let Some(stdin) = child.stdin.as_mut() { - let _ = stdin.write_all(contents.as_bytes()); - } - let _ = child.wait(); - } + let _ = self.board.borrow_mut().set_text(contents); } } pub fn create( - nsview: *mut c_void, + native_handle: *mut c_void, width: f32, height: f32, scale: f32, ) -> Option { - let ptr = NonNull::new(nsview)?; + let ptr = NonNull::new(native_handle)?; + + #[cfg(target_os = "macos")] + let backends = wgpu::Backends::METAL; + #[cfg(target_os = "windows")] + let backends = wgpu::Backends::DX12; + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + let backends = wgpu::Backends::VULKAN; let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::METAL, + backends, ..Default::default() }); - let raw_window = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)); - let raw_display = RawDisplayHandle::AppKit(AppKitDisplayHandle::new()); + #[cfg(target_os = "macos")] + let (raw_window, raw_display) = ( + RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)), + RawDisplayHandle::AppKit(AppKitDisplayHandle::new()), + ); + #[cfg(target_os = "windows")] + let (raw_window, raw_display) = { + let mut wh = Win32WindowHandle::new(std::num::NonZero::new(ptr.as_ptr() as isize).unwrap()); + ( + RawWindowHandle::Win32(wh), + RawDisplayHandle::Windows(WindowsDisplayHandle::new()), + ) + }; let target = wgpu::SurfaceTargetUnsafe::RawHandle { raw_display_handle: raw_display, @@ -181,7 +185,9 @@ pub fn render(handle: &mut ViewportHandle) { &mut handle.renderer, ); - let mut clipboard = MacClipboard; + let mut clipboard = AcordClipboard { + board: std::cell::RefCell::new(arboard::Clipboard::new().unwrap()), + }; let mut messages: Vec = Vec::new(); let mut consumed: Vec = Vec::new(); // Captured during the event scan, applied to `handle.state.mods` AFTER @@ -627,6 +633,13 @@ pub fn render(handle: &mut ViewportHandle) { handle.state.update(msg); } + // Drain any clipboard write the editor queued during update/tick. + if let Some(text) = handle.state.pending_clipboard.take() { + if let Ok(mut board) = arboard::Clipboard::new() { + let _ = board.set_text(text); + } + } + handle.state.tick(); let pending_focus = handle.state.take_pending_focus(); // Drain BEFORE the second `ui` is built — `view()` re-borrows state and