-
Notifications
You must be signed in to change notification settings - Fork 291
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
base: master
Are you sure you want to change the base?
Conversation
9bc259d
to
81aea1d
Compare
@joshka would love your input on this |
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- 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.
- 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.
There was a problem hiding this comment.
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
let mut buffer = String::new(); | ||
super::CopyToClipboard("foo") | ||
.write_ansi(&mut buffer) | ||
.unwrap(); | ||
assert_eq!(buffer, "\x1b]52;c;Zm9v\x07"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
Therefore it's maybe not worth it implementing OSC52 paste for anything other than testing.
There was a problem hiding this comment.
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:
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Stable clippy on windows understandibly warns about unclear precedence in this line.
ab8c54f
to
16bc099
Compare
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.
9131b92
to
626768a
Compare
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//! https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) | |
//! <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands>) |
//! cargo run --example copy -- --clipboard "Some String" | ||
//! | ||
//! cargo run --example copy -- --primary "Some String" | ||
//! | ||
//! cargo run --example copy -- "Some String" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//! 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") }; |
There was a problem hiding this comment.
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.
@@ -396,6 +400,62 @@ impl<T: fmt::Display> Command for SetTitle<T> { | |||
} | |||
} | |||
|
|||
/// Different clipboard classes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
anddata
). 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 namedSelectionBuffer
?
Will do.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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
let mut buffer = String::new(); | ||
super::CopyToClipboard("foo") | ||
.write_ansi(&mut buffer) | ||
.unwrap(); | ||
assert_eq!(buffer, "\x1b]52;c;Zm9v\x07"); |
There was a problem hiding this comment.
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.
Hi @joshka,
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. :) )
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.
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 My suggestion to consolidate this would be to actually move the clipboard code into a separate
I guess you're asking because a 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 I'll respond to the other items in line or implement them. Cheers and thanks again, Johannes |
Sounds good - this also simplifies the feature flag checks to just include the entire module.
Your logic there seems sound to me. |
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.