diff --git a/Cargo.lock b/Cargo.lock index 01134672f3..1b6c21dec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -661,6 +667,7 @@ name = "gitu" version = "0.39.0" dependencies = [ "arboard", + "base64", "cached", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 70a2afae9f..4ac2f73593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ strip = true arboard = { version = "3.6.1", default-features = false, features = [ "windows-sys", ] } +base64 = "0.22.1" chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } crossterm = "0.28.1" diff --git a/src/app.rs b/src/app.rs index b34a02d731..a665a3184c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,6 @@ use std::sync::Arc; use std::sync::RwLock; use std::time::Duration; -use arboard::Clipboard; use crossterm::event; use crossterm::event::Event; use crossterm::event::KeyCode; @@ -23,6 +22,7 @@ use ratatui::layout::Size; use tui_prompts::State as _; use crate::cli; +use crate::clipboard::Clipboard; use crate::cmd_log::CmdLog; use crate::cmd_log::CmdLogEntry; use crate::config::Config; @@ -87,9 +87,7 @@ impl App { let pending_menu = root_menu(&config).map(PendingMenu::init); - let clipboard = Clipboard::new() - .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}")) - .ok(); + let clipboard = Clipboard::new(config.general.use_osc52_clipboard); let mut app = Self { state: State { diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000000..65479e1061 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,105 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use std::io::{self, Write}; + +/// Trait for clipboard implementations +trait ClipboardBackend { + fn set_text(&mut self, text: &str) -> Result<(), ClipboardError>; +} + +/// Arboard-based clipboard implementation (system clipboard) +struct ArboardClipboard { + clipboard: arboard::Clipboard, +} + +impl ArboardClipboard { + fn new() -> Result { + Ok(Self { + clipboard: arboard::Clipboard::new()?, + }) + } +} + +impl ClipboardBackend for ArboardClipboard { + fn set_text(&mut self, text: &str) -> Result<(), ClipboardError> { + self.clipboard + .set_text(text) + .map_err(|e| ClipboardError::Arboard(e)) + } +} + +/// OSC 52-based clipboard implementation (terminal escape sequences) +struct Osc52Clipboard; + +impl Osc52Clipboard { + fn new() -> Self { + Self + } +} + +impl ClipboardBackend for Osc52Clipboard { + fn set_text(&mut self, text: &str) -> Result<(), ClipboardError> { + let encoded = STANDARD.encode(text); + let osc52 = format!("\x1b]52;c;{}\x07", encoded); + + // Write directly to stderr to avoid interfering with UI + io::stderr().write_all(osc52.as_bytes())?; + io::stderr().flush()?; + + Ok(()) + } +} + +/// Main clipboard wrapper that manages different backend implementations +pub(crate) struct Clipboard { + backend: Box, +} + +impl Clipboard { + /// Creates a new clipboard instance with the preferred backend. + /// If use_osc52 is true, uses OSC 52 with arboard as fallback. + /// Otherwise, uses only arboard. + pub fn new(use_osc52: bool) -> Option { + if use_osc52 { + // Prefer OSC 52, fallback to arboard + Some(Self { + backend: Box::new(Osc52Clipboard::new()), + }) + } else { + // Try arboard only + ArboardClipboard::new() + .inspect_err(|e| log::warn!("Couldn't initialize arboard clipboard: {e}")) + .ok() + .map(|cb| Self { + backend: Box::new(cb), + }) + } + } + + /// Sets text to clipboard using the configured backend. + pub fn set_text(&mut self, text: String) -> Result<(), ClipboardError> { + self.backend.set_text(&text) + } +} + +#[derive(Debug)] +pub enum ClipboardError { + Io(io::Error), + Arboard(arboard::Error), +} + +impl From for ClipboardError { + fn from(err: io::Error) -> Self { + ClipboardError::Io(err) + } +} + +impl std::fmt::Display for ClipboardError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardError::Io(err) => write!(f, "IO error: {}", err), + ClipboardError::Arboard(err) => write!(f, "Clipboard error: {}", err), + } + } +} + +impl std::error::Error for ClipboardError {} diff --git a/src/config.rs b/src/config.rs index 124fe9dddb..0e1c59d991 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,6 +37,7 @@ pub struct GeneralConfig { pub recent_commits_limit: usize, pub mouse_support: bool, pub mouse_scroll_lines: usize, + pub use_osc52_clipboard: bool, } #[derive(Default, Debug, Deserialize)] diff --git a/src/default_config.toml b/src/default_config.toml index 6483dd50ed..c7409e393b 100644 --- a/src/default_config.toml +++ b/src/default_config.toml @@ -21,6 +21,15 @@ mouse_scroll_lines = 3 # "never" - never prompt when discarding. confirm_discard = "line" +# Enable OSC 52 clipboard support for terminals that support it. +# This allows clipboard operations over SSH and in environments without +# system clipboard access. Set to true to enable. +# +# To test if your terminal supports OSC 52, run this command: +# printf '\033]52;c;%s\007' "$(echo -n 'test' | base64)" +# Then try pasting - if you see 'test', OSC 52 is supported. +use_osc52_clipboard = false + [style] # fg / bg can be either of: # - a hex value: "#707070" diff --git a/src/error.rs b/src/error.rs index 5f4ecdc560..5421a18d50 100644 --- a/src/error.rs +++ b/src/error.rs @@ -37,7 +37,7 @@ pub enum Error { ReadOid(git2::Error), ArgMustBePositiveNumber, ArgInvalidRegex(regex::Error), - Clipboard(arboard::Error), + Clipboard(crate::clipboard::ClipboardError), FindGitRev(git2::Error), NoEditorSet, GitStatus(git2::Error), diff --git a/src/lib.rs b/src/lib.rs index 2f7e720171..52e59cb7f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod app; mod bindings; pub mod cli; +mod clipboard; mod cmd_log; pub mod config; pub mod error;