Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add copying to clipboard using OSC52 #974

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

naseschwarz
Copy link
Contributor

Many terminal emulators support copying text to clipboard using ANSI OSC Ps; PT ST with Ps = 5 2, see
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands

This enables copying even through SSH and terminal multiplexers.

I'm not too sure whether you want this feature or whether src/terminal.rs is the right place for it. I had just implemented it for gitui, which uses crossterm, and thought, it would be nice to have it upstream too.

It does not come for free, as it adds a new dependency to base64 which is not yet required by any of crossterm's dependencies.

@extrawurst
Copy link
Contributor

@joshka would love your input on this

Copy link
Collaborator

@joshka joshka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General support for the idea with a few questions / thoughts. terminal seems like the right module for this unless you were to add a clipboard module, but there's only one command likely to end up there, so that feels like probably a bit unnecessary.

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CopyToClipboard<T>(pub T);

impl<T: AsRef<[u8]>> Command for CopyToClipboard<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea if there's a windows api equivalent to this?

Copy link
Contributor Author

@naseschwarz naseschwarz Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for reference: Windows Terminal supports (or will support) OSC52: https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-23-release/ . I guess, however, that this is not the default terminal for all windows applications?

As I cannot test any Windows implementation, any implementation would probably need to be contributed by someone else.

Copy link
Contributor Author

@naseschwarz naseschwarz Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've looked into this on an EC2 windows machine and concluded that it's unlikely that I will provide a winapi implementation here for two reasons:

  1. The modern windows terminal does not require WinAPI copying anymore anyhow but supports OSC52, so it would be a very niche improvement in the first place.
  2. Copying via OSC52 is really only relevant for applications running on remote hosts, e.g. via SSH, which the Windows API will never support. Other applications should really directly use the host API in order to avoid an unsupported, disabled or length-limited OSC52 on any host, be it windows, linux or macos.

Crossterm, however, uses a different metric for its decision - whether the terminal supports ANSI escape sequences or not. But because of the second reason above this is the wrong metric here. The correct metric for CopyToClipboard should be whether the application has access to any better API. It is my understanding that this is past the scope of crossterm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking. I'll let @TimonPost chime in here a bit more. Crossterm is the only reason that I've spun up a Windows VM in close to 10 years (I used to be a .NET dev, but haven't touched it in a while, so my knowledge is fairly limited in how Windows terminals have progressed in that time).

src/terminal.rs Outdated
Comment on lines 592 to 636
let mut buffer = String::new();
super::CopyToClipboard("foo")
.write_ansi(&mut buffer)
.unwrap();
assert_eq!(buffer, "\x1b]52;c;Zm9v\x07");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's a way to actually test that this works by checking the clipboard somehow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, you could spin up a terminal emulator and test it - one can request the clipboard via Ps = 5 2, Pd = '?':

      If the second parameter is a ? , xterm replies to the host
      with the selection data encoded using the same protocol.  It
      uses the first selection found by asking successively for each
      item from the list of selection parameters.

Originally, I'd guessed that this is not in the scope of these tests. However, it seems like the CI is running unit tests in predefined terminals with nocapture enabled. So, maybe for crossterm-rs, it is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should add that I would guess that having OSC52 paste mode enabled should probably be considered a security issue. I certainly would not expect untrusted hosts to extract clipboard data just because I SSH into them. Alacritty, for example, does not enable this feature by default:

image

Therefore it's maybe not worth it implementing OSC52 paste for anything other than testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing the other tests, I don't think this is practical at the moment. Other tests that actually test interaction with the terminal seem to be disabled at the moment, as they fail in the CI:

image

If there is actual interest in testing this (@TimonPost?), I'd gladly contribute to a more involved testing effort and maybe even sponsor the required compute time. For this OSC52 copy, however, I'd be quite content with the unit test against the specification. There's no space for free parameters that could break this feature here and an integration test with terminals would mostly test whether the terminals changed something. Thus, crossterm would only really profit if the tests were run against four or five popular terminals. As far as I can tell, this is not the case at the moment?

Copy link
Contributor Author

@naseschwarz naseschwarz Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, there is one free parameter, I have neglected: Size restrictions on the content. I'd wager terminals limit this size. I have not tested where this limit is, as I only intend to copy short strings. Thus, one interesting parameter to test in a system test would be to test those limits on different terminal emulators.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, some of these things do seem like they would be really difficult to test in CI. Thanks for doing the investigation though. This is not a blocker.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Therefore it's maybe not worth it implementing OSC52 paste for anything other than testing.

I think I would probably consider adding this more for apps, but make a caveat in the documentation about how this feature is often disabled by the terminal emulator, multiplexer or protocol in use due to security concerns.

@naseschwarz naseschwarz marked this pull request as draft March 14, 2025 12:39
Stable clippy on windows understandibly warns about unclear precedence
in this line.
Many terminal emulators support copying text to clipboard using
ANSI OSC Ps; PT ST with Ps = 5 2, see
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands

This enables copying even through SSH and terminal multiplexers.
@naseschwarz naseschwarz force-pushed the feature/copy-text branch 2 times, most recently from 9131b92 to 626768a Compare March 15, 2025 13:01
Copy link
Collaborator

@joshka joshka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of extra comments after reading the docs more and giving this a deeper review. Apologies that my first review was fairly surface level in comparison.

At a high level, I support the addition of this. To summarize my thoughts across the various comments raised here, I have some concern around what level of abstraction Crossterm should provide for this. I don't have any immediately concrete suggestions though, so looking for some back and forth of ideas before settling on the right approach. Feel free to shoot down any of the perspectives I've thrown here with your viewpoints - you're likely just as much or more of an expert on these than I am.


Regarding the paste side of this (passing ? as the data). It seems like it could be worth adding this usage of OSC 52 at the same time as adding this feature. WDYT?

      If the second parameter is a ? , xterm replies to the host
      with the selection data encoded using the same protocol.  It
      uses the first selection found by asking successively for each
      item from the list of selection parameters.

Also there's a clear clipboard function - worth worrying about?

      If the second parameter is neither a base64 string nor ? ,
      then the selection is cleared.

//! Demonstrates copying a string to clipboard
//!
//! This example uses OSC control sequence `Pr = 5 2` (See
//! https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands)
//! <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands>)

Comment on lines +10 to +14
//! cargo run --example copy -- --clipboard "Some String"
//!
//! cargo run --example copy -- --primary "Some String"
//!
//! cargo run --example copy -- "Some String"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! cargo run --example copy -- --clipboard "Some String"
//!
//! cargo run --example copy -- --primary "Some String"
//!
//! cargo run --example copy -- "Some String"
//! ```
//! cargo run --example copy -- --clipboard "Some String"
//! cargo run --example copy -- --primary "Some String"
//! cargo run --example copy -- "Some String"
//! ```

#[macro_export]
#[doc(hidden)]
macro_rules! osc {
($( $l:expr ),*) => { concat!("\x1B]", $( $l ),*, "\x07") };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably go with the ending with ST (ESC \ = 0x1B 0x5C) instead of BEL here. My understanding is BEL is historic, while ST is standard. I'm not sure of how well either are supported across terminals though.

https://en.wikipedia.org/wiki/ANSI_escape_code#OSC

@@ -396,6 +400,62 @@ impl<T: fmt::Display> Command for SetTitle<T> {
}
}

/// Different clipboard classes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Different clipboard classes
/// Different clipboard classes
///
/// See <https://specifications.freedesktop.org/clipboard-spec/latest/> for more info

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClipboardDestination {
/// Default clipboard when using Ctrl+C or Ctrl+V
Clipboard,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be used as reasonable default to help make it easy for users not to have to think?

Edit: probably not - thoughts about the value in the command being a Vec added after this comment. Left this here for following the train of thought.

/// See examples/copy.rs for a working example.
#[cfg(feature = "clipboard")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CopyToClipboard<T>(pub T, pub ClipboardDestination);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that once you have more than 1 field in a struct like this, I'd tend to want to give the values names for better clarity in the API (e.g. what about naming the fields selection and data). Doing this gives a place to hang doc comments off to help a user understand what the intended values will look like.

Additionally, based on the spec docs, it sounds like this can accept 0 or more destinations, so it might be worth making this a Vec<ClipboardSelection> to support that. It should be noted that the comment about how xterm handles copying when this is not provided by defaulting to copy to the clipboard/primary/cut buffer 0 suggests that this should probably be also able to work without passing the selection. This implies adding a constructor or something like that that doesn't require choosing the selection. WDYT?

That said, there's some tension between the spec default (s 0 = selection buffer + cut buffer 0) and the suggestions from the clipboard spec doc (primary + clipboard), so I'm not sure how best to resolve those in a way that makes sense for users.

Would naming this CopySelection make sense here, and then the parameter be named SelectionBuffer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that once you have more than 1 field in a struct like this, I'd tend to want to give the values names for better clarity in the API (e.g. what about naming the fields selection and data). Doing this gives a place to hang doc comments off to help a user understand what the intended values will look like.

I'll gladly change this. :)

Additionally, based on the spec docs, it sounds like this can accept 0 or more destinations, so it might be worth making this a Vec<ClipboardSelection> to support that. It should be noted that the comment about how xterm handles copying when this is not provided by defaulting to copy to the clipboard/primary/cut buffer 0 suggests that this should probably be also able to work without passing the selection. This implies adding a constructor or something like that that doesn't require choosing the selection. WDYT?

I'm up for making this a Vec<...> and providing constructors - this should improve readability.

That said, there's some tension between the spec default (s 0 = selection buffer + cut buffer 0) and the suggestions from the clipboard spec doc (primary + clipboard), so I'm not sure how best to resolve those in a way that makes sense for users.

I'm concerned about implicitly exposing an internal and remote OSC52 default if vec![] is passed. This behavior at least warrants documenting - in particular as the concrete result much more depends on whether a terminal emulator implements this default correctly.

I was about to write that it's not worth worrying about this, as X is being phased out and wayland only supports clipboard and primary. Then I learned that Mac OS X actually still seems (or at least in 2012 did) to support cut buffers.

Two suggestions. Either we disallow empty vectors as a clipboard selection parameter or we deal with it by documenting that the behavior when passing a vec![] is entirely defined by the terminal emulator.

Would naming this CopySelection make sense here, and then the parameter be named SelectionBuffer?

Will do.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deal with it by documenting that the behavior when passing a vec![] is entirely defined by the terminal emulator.

I think this is probably the right approach

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CopyToClipboard<T>(pub T);

impl<T: AsRef<[u8]>> Command for CopyToClipboard<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking. I'll let @TimonPost chime in here a bit more. Crossterm is the only reason that I've spun up a Windows VM in close to 10 years (I used to be a .NET dev, but haven't touched it in a while, so my knowledge is fairly limited in how Windows terminals have progressed in that time).

src/terminal.rs Outdated
Comment on lines 592 to 636
let mut buffer = String::new();
super::CopyToClipboard("foo")
.write_ansi(&mut buffer)
.unwrap();
assert_eq!(buffer, "\x1b]52;c;Zm9v\x07");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Therefore it's maybe not worth it implementing OSC52 paste for anything other than testing.

I think I would probably consider adding this more for apps, but make a caveat in the documentation about how this feature is often disabled by the terminal emulator, multiplexer or protocol in use due to security concerns.

@naseschwarz
Copy link
Contributor Author

naseschwarz commented Mar 16, 2025

Hi @joshka,

Lots of extra comments after reading the docs more and giving this a deeper review. Apologies that my first review was fairly surface level in comparison.

I appreciate this very much. Thank you for taking the time. (Also, I know the feeling of having to double down on a review very well. No worries. :) )

At a high level, I support the addition of this. To summarize my thoughts across the various comments raised here, I have some concern around what level of abstraction Crossterm should provide for this.

I've had these, too. This whole change feels odd as clipboard support breaks a barrier in so far as that it is very different from the usual presentation and input mechanisms supplied by terminals and in this case crossterm. It is only really relevant to applications as a means for copying from remote hosts, where the only line is a TTY. Thus, while odd, signalling the copy operation in-band is the go-to solution.

Regarding the paste side of this (passing ? as the data). It seems like it could be worth adding this usage of OSC 52 at the same time as adding this feature. WDYT?

I don't have much to add to my previously stated opinion. There is essentially no need for paste (as pasting can be done through the terminal emulator and normal input) and normalizing its use is a huge security concern. You can't even use it to cross-check OSC52, as some terminal emulators disable OSC52 paste while having OSC52 copy enabled. (And after this has been merged I'll probably try to get alacritty's behavior into more emulators. I was quite surprised by how many emulators actually allow paste.)

(I agree that there is a small impedance mismatch. If I run my favorite editor and if that were to use crossterm on a remote host, I can't just type "+p in order to paste from my host's clipboard. I'd argue that inconvenience is small, given the price.)

My suggestion to consolidate this would be to actually move the clipboard code into a separate clipboard.rs and make rename ClipboardDestination to ClipboardSelection such that the clipboard features stand out a bit more and it'll be easy to add PasteFromClipboard later, if one wants to do so. Would that be alright?

Also there's a clear clipboard function - worth worrying about?

      If the second parameter is neither a base64 string nor ? ,
      then the selection is cleared.

I guess you're asking because a CopyToClipboard("", _) will base64-encode to an empty second parameter, i.e. clear the clipboard instead of copying an empty string? I would have guessed that from a user's standpoint an empty clipboard and a clipboard containing an empty string would be equivalent.

However, it is true that for clipboard protocols, there generally is a difference between being cleared and containing an empty string.

That being said, I'm not sure how to address this given OSC52's specification. A wider API would only be relevant if there is a plan to support other clipboard protocols (like wayland, X.org, winapi, whatever macos is doing and whatever we want to implement ourselves) in the future. My view as a new contributor is that this is not in crossterm's scope.

If you disagree, we should discuss this, though. Frankly, I do see a potential use case for copying a text/uri-list that contains sftp-URIs for pasting into your favorite file browser for file download through SSH. The only limit here is that associating a MIME type is not supported by OSC52. That does not really keep us from widening the interface to something resembling CopyToClipboard(T, MimeType, ClipboardSelection) in order to support that in the future.

I'll respond to the other items in line or implement them.

Cheers and thanks again,

Johannes

@joshka
Copy link
Collaborator

joshka commented Mar 16, 2025

My suggestion to consolidate this would be to actually move the clipboard code into a separate clipboard.rs and make rename ClipboardDestination to ClipboardSelection such that the clipboard features stand out a bit more and it'll be easy to add PasteFromClipboard later, if one wants to do so. Would that be alright?

Sounds good - this also simplifies the feature flag checks to just include the entire module.

That being said, I'm not sure how to address this given OSC52's specification. A wider API would only be relevant if there is a plan to support other clipboard protocols (like wayland, X.org, winapi, whatever macos is doing and whatever we want to implement ourselves) in the future. My view as a new contributor is that this is not in crossterm's scope.

Your logic there seems sound to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants