Clipboard handling needed revision in preperation for Windows Support.

This commit is contained in:
jess 2026-04-17 13:07:08 -07:00
parent 03284a694e
commit 17d513c62f
3 changed files with 59 additions and 55 deletions

View File

@ -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"

View File

@ -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<String>,
}
/// 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

View File

@ -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<arboard::Clipboard>,
}
impl clipboard::Clipboard for MacClipboard {
impl clipboard::Clipboard for AcordClipboard {
fn read(&self, _kind: clipboard::Kind) -> Option<String> {
// `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<ViewportHandle> {
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<Message> = Vec::new();
let mut consumed: Vec<usize> = 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