From 4ffad110b65cd7a84ad2bcf4e825684f65a15e07 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 21 Dec 2023 22:13:17 -0700 Subject: [PATCH] Add copy/paste --- src/main.rs | 97 +++++++++++++++++++++++++++++++++++---------- src/terminal.rs | 22 ++++++++++ src/terminal_box.rs | 18 ++++++--- 3 files changed, 111 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8de965e..f04eb2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,15 @@ use alacritty_terminal::{ config::Config as TermConfig, event::Event as TermEvent, term::color::Colors as TermColors, tty, }; use cosmic::{ - app::{Command, Core, Settings}, + app::{message, Command, Core, Settings}, cosmic_theme, executor, iced::{ + clipboard, event, futures::SinkExt, + keyboard::{Event as KeyEvent, KeyCode, Modifiers}, subscription::{self, Subscription}, widget::row, - window, Alignment, Length, + window, Alignment, Event, Length, }, iced_core::Size, style, @@ -58,6 +60,9 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { + Copy, + Paste, + PasteValue(String), TabActivate(segmented_button::Entity), TabClose(segmented_button::Entity), TabNew, @@ -118,6 +123,33 @@ impl cosmic::Application for App { /// Handle application events here. fn update(&mut self, message: Self::Message) -> Command { match message { + Message::Copy => { + if let Some(terminal) = self + .tab_model + .data::>(self.tab_model.active()) + { + let terminal = terminal.lock().unwrap(); + let term = terminal.term.lock(); + if let Some(text) = term.selection_to_string() { + return clipboard::write(text); + } + } + } + Message::Paste => { + return clipboard::read(|value_opt| match value_opt { + Some(value) => message::app(Message::PasteValue(value)), + None => message::none(), + }); + } + Message::PasteValue(value) => { + if let Some(terminal) = self + .tab_model + .data::>(self.tab_model.active()) + { + let terminal = terminal.lock().unwrap(); + terminal.paste(value); + } + } Message::TabActivate(entity) => { self.tab_model.activate(entity); return self.update_title(); @@ -279,26 +311,51 @@ impl cosmic::Application for App { fn subscription(&self) -> Subscription { struct TerminalEventWorker; - subscription::channel( - TypeId::of::(), - 100, - |mut output| async move { - let (event_tx, mut event_rx) = mpsc::channel(100); - output.send(Message::TermEventTx(event_tx)).await.unwrap(); - - // Create first terminal tab - output.send(Message::TabNew).await.unwrap(); - - while let Some((entity, event)) = event_rx.recv().await { - output - .send(Message::TermEvent(entity, event)) - .await - .unwrap(); + Subscription::batch([ + event::listen_with(|event, _status| match event { + Event::Keyboard(KeyEvent::KeyPressed { + key_code: KeyCode::C, + modifiers, + }) => { + if modifiers == Modifiers::CTRL | Modifiers::SHIFT { + Some(Message::Copy) + } else { + None + } } + Event::Keyboard(KeyEvent::KeyPressed { + key_code: KeyCode::V, + modifiers, + }) => { + if modifiers == Modifiers::CTRL | Modifiers::SHIFT { + Some(Message::Paste) + } else { + None + } + } + _ => None, + }), + subscription::channel( + TypeId::of::(), + 100, + |mut output| async move { + let (event_tx, mut event_rx) = mpsc::channel(100); + output.send(Message::TermEventTx(event_tx)).await.unwrap(); - panic!("terminal event channel closed"); - }, - ) + // Create first terminal tab + output.send(Message::TabNew).await.unwrap(); + + while let Some((entity, event)) = event_rx.recv().await { + output + .send(Message::TermEvent(entity, event)) + .await + .unwrap(); + } + + panic!("terminal event channel closed"); + }, + ), + ]) } } diff --git a/src/terminal.rs b/src/terminal.rs index 19c61cd..edb9abe 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -8,6 +8,7 @@ use alacritty_terminal::{ term::{ cell::Flags, color::{Colors, Rgb}, + TermMode, }, tty, Term, }; @@ -207,6 +208,27 @@ impl Terminal { self.scroll(TerminalScroll::Bottom); } + pub fn paste(&self, value: String) { + // This code is ported from alacritty + let bracketed_paste = { + let term = self.term.lock(); + term.mode().contains(TermMode::BRACKETED_PASTE) + }; + if bracketed_paste { + self.input_no_scroll(&b"\x1b[200~"[..]); + self.input_no_scroll(value.replace('\x1b', "").into_bytes()); + self.input_scroll(&b"\x1b[201~"[..]); + } else { + // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish + // pasted data from keystrokes. + // In theory, we should construct the keystrokes needed to produce the data we are + // pasting... since that's neither practical nor sensible (and probably an impossible + // task to solve in a general way), we'll just replace line breaks (windows and unix + // style) with a single carriage return (\r, which is what the Enter key produces). + self.input_scroll(value.replace("\r\n", "\r").replace('\n', "\r").into_bytes()); + } + } + pub fn resize(&mut self, width: u32, height: u32) { if width != self.size.width || height != self.size.height { let instant = Instant::now(); diff --git a/src/terminal_box.rs b/src/terminal_box.rs index a04dabb..e2ad578 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -224,7 +224,7 @@ where background_color.r() as f32 / 255.0, background_color.g() as f32 / 255.0, background_color.b() as f32 / 255.0, - background_color.a() as f32 / 255.0 + background_color.a() as f32 / 255.0, ), ); } @@ -405,36 +405,42 @@ where state.modifiers.logo(), state.modifiers.control(), state.modifiers.alt(), + state.modifiers.shift(), ) { - (true, _, _) => { + (true, _, _, _) => { // Ignore super } - (false, true, _) => { + (false, true, _, false) => { // Handle ctrl for control characters (Ctrl-A to Ctrl-Z) if character.is_control() { let mut buf = [0, 0, 0, 0]; let str = character.encode_utf8(&mut buf); terminal.input_scroll(str.as_bytes().to_vec()); + status = Status::Captured; } } - (false, false, true) => { + (false, true, _, true) => { + // Ignore ctrl+shift + } + (false, false, true, _) => { if !character.is_control() { // Handle alt for non-control characters let mut buf = [0x1B, 0, 0, 0, 0]; let str = character.encode_utf8(&mut buf[1..]); terminal.input_scroll(str.as_bytes().to_vec()); + status = Status::Captured; } } - (false, false, false) => { + (false, false, false, _) => { // Handle no modifiers for non-control characters if !character.is_control() { let mut buf = [0, 0, 0, 0]; let str = character.encode_utf8(&mut buf); terminal.input_scroll(str.as_bytes().to_vec()); + status = Status::Captured; } } } - status = Status::Captured; } Event::Mouse(MouseEvent::ButtonPressed(button)) => { if let Some(p) = cursor_position.position_in(layout.bounds()) {