use std::io;
use std::time::Duration;
use crossterm::event as ct;
use crossterm::execute;
use super::app::App;
use super::clipboard::{OSC52_QUERY, decode_base64};
use super::engine::Engine;
const OSC52_WAIT: Duration = Duration::from_millis(200);
const OSC52_LATE: Duration = Duration::from_secs(30);
#[derive(Default)]
pub(super) struct TerminalClipboard {
asked: Option<Duration>,
waiting: bool,
reply: Osc52Reply,
}
impl TerminalClipboard {
pub(super) fn update<A: App>(&mut self, engine: &mut Engine<A>, now: Duration) -> io::Result<()> {
engine.poll_clipboard(now);
if std::mem::take(&mut engine.terminal_query) {
execute!(io::stdout(), crossterm::style::Print(OSC52_QUERY))?;
self.asked = Some(now);
self.waiting = true;
}
if self.waiting && self.asked.is_some_and(|asked| now >= asked + OSC52_WAIT) {
self.waiting = false;
engine.terminal_clipboard(None, now);
}
Ok(())
}
pub(super) fn deadline(&self) -> Option<Duration> {
self.asked.filter(|_| self.waiting).map(|asked| asked + OSC52_WAIT)
}
pub(super) fn filter<A: App>(
&mut self,
event: ct::Event,
more: bool,
engine: &mut Engine<A>,
now: Duration,
) -> Vec<ct::Event> {
if self.asked.is_none_or(|asked| now >= asked + OSC52_LATE) && !self.reply.is_open() {
return vec![event];
}
match self.reply.feed(event, more) {
Feed::Pending => Vec::new(),
Feed::Answer(text) => {
if std::mem::take(&mut self.waiting) {
engine.terminal_clipboard(text, now);
}
Vec::new()
}
Feed::Pass(events) => events,
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Feed {
Pending,
Answer(Option<String>),
Pass(Vec<ct::Event>),
}
#[derive(Debug, Default)]
struct Osc52Reply {
held: Vec<ct::Event>,
head: String,
data: Option<String>,
}
impl Osc52Reply {
fn is_open(&self) -> bool {
!self.held.is_empty()
}
fn feed(&mut self, event: ct::Event, more: bool) -> Feed {
let ct::Event::Key(key) = &event else {
return self.pass(event);
};
let alt = key.modifiers == ct::KeyModifiers::ALT;
let ctrl = key.modifiers == ct::KeyModifiers::CONTROL;
let plain = !key.modifiers.intersects(ct::KeyModifiers::ALT | ct::KeyModifiers::CONTROL);
match (&mut self.data, key.code) {
(None, ct::KeyCode::Char(']')) if alt && more && self.held.is_empty() => {
self.held.push(event);
Feed::Pending
}
(None, ct::KeyCode::Char(c)) if plain && !self.held.is_empty() => {
self.head.push(c);
self.held.push(event);
if !"52;".starts_with(self.head.as_str()) && !self.head.starts_with("52;") {
return self.pass_held();
}
if self.head.len() > 3 && c == ';' {
self.data = Some(String::new());
} else if self.head.len() > 3 && !c.is_ascii_alphanumeric() {
return self.pass_held();
}
Feed::Pending
}
(Some(_), ct::KeyCode::Char('g')) if ctrl => self.finish(),
(Some(_), ct::KeyCode::Char('\\')) if alt => self.finish(),
(Some(data), ct::KeyCode::Char(c)) if plain => {
data.push(c);
Feed::Pending
}
(Some(_), _) => {
*self = Self::default();
Feed::Pass(vec![event])
}
_ => self.pass(event),
}
}
fn finish(&mut self) -> Feed {
let data = std::mem::take(self).data.unwrap_or_default();
Feed::Answer(decode_base64(&data).filter(|text| !text.is_empty()))
}
fn pass(&mut self, event: ct::Event) -> Feed {
let mut events = std::mem::take(self).held;
events.push(event);
Feed::Pass(events)
}
fn pass_held(&mut self) -> Feed {
Feed::Pass(std::mem::take(self).held)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn keys(bytes: &str) -> Vec<ct::Event> {
let mut events = Vec::new();
let mut chars = bytes.chars();
while let Some(c) = chars.next() {
let (code, mods) = match c {
'\x1b' => (chars.next().map_or(ct::KeyCode::Esc, ct::KeyCode::Char), ct::KeyModifiers::ALT),
'\x07' => (ct::KeyCode::Char('g'), ct::KeyModifiers::CONTROL),
c if c.is_uppercase() => (ct::KeyCode::Char(c), ct::KeyModifiers::SHIFT),
c => (ct::KeyCode::Char(c), ct::KeyModifiers::NONE),
};
events.push(ct::Event::Key(ct::KeyEvent::new(code, mods)));
}
events
}
fn feed_all(reply: &mut Osc52Reply, events: Vec<ct::Event>) -> Vec<Feed> {
let count = events.len();
events.into_iter().enumerate().map(|(index, event)| reply.feed(event, index + 1 < count)).collect()
}
#[test]
fn picks_the_osc52_answer_out_of_the_input() {
let mut reply = Osc52Reply::default();
let feeds = feed_all(&mut reply, keys("\x1b]52;c;ZGVwbG95LWFwaQ==\x07"));
assert!(feeds[..feeds.len() - 1].iter().all(|feed| *feed == Feed::Pending), "{feeds:?}");
assert_eq!(feeds.last(), Some(&Feed::Answer(Some("deploy-api".into()))));
let feeds = feed_all(&mut reply, keys("\x1b]52;c;w6dheQ==\x1b\\"));
assert_eq!(feeds.last(), Some(&Feed::Answer(Some("çay".into()))), "ST ends it as well as BEL");
let feeds = feed_all(&mut reply, keys("\x1b]52;c;\x07"));
assert_eq!(feeds.last(), Some(&Feed::Answer(None)), "an empty clipboard");
}
#[test]
fn keys_that_only_look_like_an_answer_are_handed_back() {
let mut reply = Osc52Reply::default();
let feeds = feed_all(&mut reply, keys("\x1b]5x"));
assert_eq!(feeds[..2], [Feed::Pending, Feed::Pending]);
assert_eq!(feeds[2], Feed::Pass(keys("\x1b]5x")), "alt+] 5 x is typed input");
let alone = keys("\x1b]").remove(0);
assert_eq!(reply.feed(alone.clone(), false), Feed::Pass(vec![alone]), "a lone alt+] is a key");
let plain = keys("q").remove(0);
assert_eq!(reply.feed(plain.clone(), true), Feed::Pass(vec![plain]));
}
}