Skip to content

Commit 0343178

Browse files
committed
support clipboard copy
1 parent ab5649d commit 0343178

File tree

7 files changed

+329
-6
lines changed

7 files changed

+329
-6
lines changed

Cargo.lock

Lines changed: 175 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "svt"
3-
version = "25.12.2"
3+
version = "25.12.3"
44
edition = "2024"
55
description = "Simple Viewer in Terminal - sxiv-like terminal image viewer"
66

@@ -15,3 +15,4 @@ flate2 = "1.0"
1515
serde = { version = "1", features = ["derive"] }
1616
toml = "0.8"
1717
dirs = "5"
18+
arboard = "3"

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ A minimal & fast terminal image viewer written in Rust with sxiv-like keybinding
1010

1111
- **Fast** - Zlib compression, prefetch, and render cache for instant navigation
1212
- **Keyboard-driven** - sxiv/vim-like keybindings with count support
13+
- **Clipboard** - Copy path (OSC 52, works over SSH) or image to clipboard (local/X11 only)
1314
- **Flexible** - Fit/Normal display modes, works over SSH with Tmux
1415
- **KGP** - Kitty Graphics Protocol for high-quality image rendering
1516

@@ -66,6 +67,8 @@ svt ~/photos/*.jpg
6667
| `G` | Last image |
6768
| `f` | Toggle fit |
6869
| `r` | Reload (clear cache) |
70+
| `y` | Copy path to clipboard (OSC 52, works over SSH) |
71+
| `Y` | Copy image to clipboard (local/X11 only) |
6972
| `q` | Quit |
7073

7174
Vim-like counts are supported (e.g. `5j`, `10G`).

docs/architecture.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,27 @@ These invariants must be preserved when modifying the codebase:
118118
5. **Transmit must complete once started**
119119
- Skip cancellation during active transmission (`is_transmitting()`)
120120
- Ensures terminal receives complete image data
121+
122+
## Clipboard Support
123+
124+
`svt` provides two clipboard copy methods via `y` and `Y` keys:
125+
126+
### Path Copy (`y` key)
127+
128+
Uses **OSC 52** escape sequence to copy the image path as text:
129+
130+
- Format: `\x1b]52;c;<base64>\x07`
131+
- Works over SSH (terminal interprets the sequence locally)
132+
- Tmux-aware: wraps in `\x1bPtmux;...\x1b\\` when `$TMUX` is set
133+
134+
Implementation: `WriterRequest::CopyToClipboard` in `src/sender.rs`
135+
136+
### Image Copy (`Y` key)
137+
138+
Uses **arboard** crate to copy image data via OS clipboard API:
139+
140+
- Converts image to RGBA and sends to system clipboard
141+
- Works on local machine and X11-forwarded SSH sessions
142+
- Does NOT work on headless SSH (no display server)
143+
144+
Implementation: `copy_image_to_clipboard()` in `src/app.rs`

src/app.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,44 @@ impl App {
517517
});
518518
}
519519

520+
/// Copy the current image's absolute path to clipboard via OSC 52.
521+
pub fn copy_path_to_clipboard(&self) -> bool {
522+
let Some(path) = self.current_path() else {
523+
return false;
524+
};
525+
let Some(path_str) = path.to_str() else {
526+
return false;
527+
};
528+
self.writer.send(WriterRequest::CopyToClipboard {
529+
data: path_str.as_bytes().to_vec(),
530+
is_tmux: self.is_tmux,
531+
});
532+
true
533+
}
534+
535+
/// Copy the current image data to clipboard (local only, uses OS API).
536+
pub fn copy_image_to_clipboard(&self) -> bool {
537+
use arboard::{Clipboard, ImageData};
538+
539+
let Some(path) = self.current_path() else {
540+
return false;
541+
};
542+
let Ok(img) = image::open(path) else {
543+
return false;
544+
};
545+
let rgba = img.to_rgba8();
546+
let (width, height) = rgba.dimensions();
547+
let image_data = ImageData {
548+
width: width as usize,
549+
height: height as usize,
550+
bytes: rgba.into_raw().into(),
551+
};
552+
let Ok(mut clipboard) = Clipboard::new() else {
553+
return false;
554+
};
555+
clipboard.set_image(image_data).is_ok()
556+
}
557+
520558
pub fn current_image_name(&self) -> String {
521559
self.images
522560
.get(self.current_index)

0 commit comments

Comments
 (0)