use super::*;
pub(super) const PASTE_BURST: usize = 2;
pub(super) const PASTE_GAP: Duration = Duration::from_millis(20);
#[cfg(windows)]
pub(super) fn reconcile_paste(
reconstructed: String,
clipboard: &mut Option<arboard::Clipboard>,
) -> String {
match read_clipboard_text(clipboard) {
Some(clip) if paste_projection_matches(&clip, &reconstructed) => clip,
_ => reconstructed,
}
}
#[cfg(not(windows))]
pub(super) fn reconcile_paste(
reconstructed: String,
_clipboard: &mut Option<arboard::Clipboard>,
) -> String {
reconstructed
}
#[cfg(windows)]
pub(super) fn paste_projection_matches(clipboard: &str, reconstructed: &str) -> bool {
fn normalize(s: &str) -> String {
s.replace("\r\n", "\n")
.replace('\r', "\n")
.replace('\t', " ")
}
let projected: String = clipboard
.chars()
.filter(|c| (*c as u32) <= 0xFFFF)
.collect();
!reconstructed.is_empty() && normalize(&projected) == normalize(reconstructed)
}
pub(super) fn collect_press(batch: &mut Vec<Event>, ev: Event) {
if let Event::Key(k) = &ev
&& k.kind != KeyEventKind::Press
{
return;
}
batch.push(ev);
}
pub(super) fn paste_char(key: &KeyEvent) -> Option<char> {
if key.kind != KeyEventKind::Press
|| key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
{
return None;
}
match key.code {
KeyCode::Char(c) => Some(c),
KeyCode::Enter => Some('\r'),
KeyCode::Tab => Some('\t'),
_ => None,
}
}
pub(super) enum Chunk {
Paste(String),
Event(Event),
}
pub(super) fn chunk_batch(batch: Vec<Event>) -> Vec<Chunk> {
let mut out = Vec::new();
let mut iter = batch.into_iter().peekable();
while let Some(ev) = iter.next() {
if let Event::Key(key) = &ev
&& let Some(first) = paste_char(key)
{
let mut run = String::new();
run.push(first);
while let Some(Event::Key(k)) = iter.peek() {
match paste_char(k) {
Some(c) => {
run.push(c);
iter.next();
}
None => break,
}
}
if run.chars().count() >= 2 {
out.push(Chunk::Paste(run));
continue;
}
}
out.push(Chunk::Event(ev));
}
out
}
pub(super) fn process_input_batch(
batch: Vec<Event>,
screen: &mut ChatScreen,
active: &mut ActiveScreen,
help: &mut HelpOverlay,
back: &mut Option<Back>,
cmd_tx: &UnboundedSender<AppCommand>,
clipboard: &mut Option<arboard::Clipboard>,
) -> bool {
let mut quit = false;
for chunk in chunk_batch(batch) {
match chunk {
Chunk::Paste(_) | Chunk::Event(Event::Paste(_) | Event::Mouse(_))
if help.open.is_some() => {}
Chunk::Paste(text) | Chunk::Event(Event::Paste(text)) => {
let text = reconcile_paste(text, clipboard);
active.handle_paste(screen, &text);
}
Chunk::Event(Event::Key(key)) => {
if handle_key_event(key, screen, active, help, back, cmd_tx, clipboard) {
quit = true;
}
}
Chunk::Event(Event::Mouse(mouse)) if active.is_chat() => {
if let Some(intent) = screen.handle_mouse(mouse) {
flush_draft(screen, cmd_tx);
if dispatch(intent, cmd_tx, screen, active, back) {
quit = true;
}
}
}
Chunk::Event(_) => {}
}
}
quit
}
fn flush_draft(screen: &mut ChatScreen, cmd_tx: &UnboundedSender<AppCommand>) {
if let Some(draft) = screen.take_dirty_draft() {
let _ = cmd_tx.send(AppCommand::SetDraft(draft));
}
}
fn handle_key_event(
key: KeyEvent,
screen: &mut ChatScreen,
active: &mut ActiveScreen,
help: &mut HelpOverlay,
back: &mut Option<Back>,
cmd_tx: &UnboundedSender<AppCommand>,
clipboard: &mut Option<arboard::Clipboard>,
) -> bool {
if let Some(state) = help.open.as_mut() {
return match state.handle_key(&key) {
HelpKeyOutcome::Quit => true,
HelpKeyOutcome::Close => {
help.close();
false
}
HelpKeyOutcome::Handled => false,
};
}
if key.code == KeyCode::F(1) {
help.open_for(help_context(active));
return false;
}
let intent = match active {
ActiveScreen::Chat => screen.handle_key(key).map(AnyIntent::Chat),
ActiveScreen::ChatList(list) => list.handle_key(key).map(AnyIntent::List),
ActiveScreen::Settings(settings) => settings.handle_key(key).map(AnyIntent::Settings),
ActiveScreen::SelfModel(view) => view.handle_key(key).map(AnyIntent::SelfModel),
ActiveScreen::Search(search) => search.handle_key(key).map(AnyIntent::Search),
ActiveScreen::Changes(changes) => changes.handle_key(key).map(AnyIntent::Changes),
ActiveScreen::Tasks(tasks) => tasks.handle_key(key).map(AnyIntent::Tasks),
};
if intent.is_some() {
flush_draft(screen, cmd_tx);
}
match intent {
Some(AnyIntent::Chat(ChatIntent::CopyToClipboard(text))) => {
let report = copy_text(clipboard, &text, screen.clipboard_osc52());
let (message, failed) = report.message(screen.loc());
match report {
CopyReport {
local: Ok(()),
terminal: TerminalCopy::NotTried,
} => {}
_ if failed => screen.push_error(&message),
_ => screen.push_note(&message),
}
false
}
Some(AnyIntent::Chat(ChatIntent::PasteImage { text_fallback })) => {
paste_from_clipboard(text_fallback, screen, active, cmd_tx, clipboard);
false
}
Some(AnyIntent::Chat(ChatIntent::OpenHelp)) => {
help.open_for(HelpContext::Chat);
false
}
Some(intent) => dispatch_any(intent, cmd_tx, screen, active, back),
None => false,
}
}
fn paste_from_clipboard(
text_fallback: bool,
screen: &mut ChatScreen,
active: &mut ActiveScreen,
cmd_tx: &UnboundedSender<AppCommand>,
clipboard: &mut Option<arboard::Clipboard>,
) {
if let Some((width, height, rgba)) = clipboard_image(clipboard) {
let _ = cmd_tx.send(AppCommand::ImagePaste(Box::new(ClipboardImage {
width,
height,
rgba,
})));
return;
}
if text_fallback {
if let Some(text) = clipboard_text(clipboard) {
active.handle_paste(screen, &text);
}
return;
}
let loc = screen.loc();
screen.set_image_progress(crate::features::image_command::ImageProgress::Failed(
loc.t("ui.err.image_no_clipboard").to_string(),
));
}