Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions rust/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ struct ClipboardCommand {
args: &'static [&'static str],
}

// Windows PowerShell talks to a redirected pipe in the console's OEM code page (437 or 850), not
// UTF-8, so a selection like "Grão moído" comes back as "Grao mo\u{fffd}do" and text we write
// reaches the clipboard as mojibake. Reading pins stdout to UTF-8 without a BOM. Writing decodes
// the raw stdin bytes as UTF-8 itself, because setting [Console]::InputEncoding does not change
// how `$input` is decoded. Like `$input | Set-Clipboard` did, it drops one trailing line break and
// joins the lines with CRLF.
//
// Not `#[cfg(windows)]`: the `if cfg!(windows)` branches below are still compiled elsewhere, so
// the names must exist on every platform.
const WINDOWS_READ_SCRIPT: &str =
"[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false; Get-Clipboard -Raw";
const WINDOWS_WRITE_SCRIPT: &str = "$m = New-Object System.IO.MemoryStream; [Console]::OpenStandardInput().CopyTo($m); $t = [System.Text.Encoding]::UTF8.GetString($m.ToArray()) -replace '\\r?\\n\\z', ''; if ($t) { Set-Clipboard -Value ($t -replace '(?<!\\r)\\n', [Environment]::NewLine) } else { Set-Clipboard -Value $null }";

fn read_commands() -> Vec<ClipboardCommand> {
if cfg!(target_os = "macos") {
return vec![ClipboardCommand {
Expand All @@ -23,7 +36,7 @@ fn read_commands() -> Vec<ClipboardCommand> {
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-Clipboard -Raw",
WINDOWS_READ_SCRIPT,
],
}];
}
Expand Down Expand Up @@ -57,7 +70,7 @@ fn write_commands() -> Vec<ClipboardCommand> {
"-NoProfile",
"-NonInteractive",
"-Command",
"$input | Set-Clipboard",
WINDOWS_WRITE_SCRIPT,
],
}];
}
Expand Down Expand Up @@ -138,4 +151,63 @@ mod tests {
assert_ne!(read_commands().len(), 0);
assert_ne!(write_commands().len(), 0);
}

// These two drive the real system clipboard, so they are `#[ignore]`d. Run them on Windows
// with `cargo test clipboard -- --ignored`. The other side of each check is a separate
// PowerShell that moves UTF-16 code units as plain ASCII numbers, so the result does not
// depend on any code page. Round-tripping `write_clipboard` into `read_clipboard` would not
// catch this bug: the two code-page errors cancel out.
//
// The read test fails without the fix. The write test only fails when powershell.exe starts
// with a console of its own, as it does when Herdr runs the plugin with no console attached;
// from a terminal the child shares that terminal's console and can pass without the fix.
#[cfg(windows)]
mod windows_utf8 {
#![allow(clippy::expect_used, reason = "tests assert by panicking")]
use super::super::*;

const TEXT: &str = "Grão moído, açúcar — ✓";

fn powershell(script: &str) -> String {
let output = process("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", script])
.stdin(Stdio::null())
.output()
.expect("run powershell.exe");
assert!(
output.status.success(),
"powershell failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}

fn units() -> Vec<u16> {
TEXT.encode_utf16().collect()
}

#[test]
#[ignore = "overwrites the system clipboard"]
fn reads_non_ascii_text_copied_by_another_program() {
let list: Vec<String> = units().iter().map(u16::to_string).collect();
powershell(&format!(
"Set-Clipboard -Value (-join ([char[]]({})))",
list.join(",")
));
let read = read_clipboard().expect("read the clipboard");
assert_eq!(read.trim_end(), TEXT);
}

#[test]
#[ignore = "overwrites the system clipboard"]
fn writes_non_ascii_text_other_programs_can_read() {
write_clipboard(TEXT).expect("write the clipboard");
let seen: Vec<u16> =
powershell("(Get-Clipboard -Raw).ToCharArray() | ForEach-Object { [int]$_ }")
.lines()
.map(|line| line.trim().parse().expect("a UTF-16 code unit"))
.collect();
assert_eq!(seen, units());
}
}
}
Loading