Skip to content
Open
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
105 changes: 105 additions & 0 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -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<Self, arboard::Error> {
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<dyn ClipboardBackend>,
}

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<Self> {
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<io::Error> 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 {}
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
9 changes: 9 additions & 0 deletions src/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod app;
mod bindings;
pub mod cli;
mod clipboard;
mod cmd_log;
pub mod config;
pub mod error;
Expand Down