Add copy/paste

This commit is contained in:
Jeremy Soller 2023-12-21 22:13:17 -07:00
parent c2fb3573d5
commit 4ffad110b6
3 changed files with 111 additions and 26 deletions

View file

@ -5,13 +5,15 @@ use alacritty_terminal::{
config::Config as TermConfig, event::Event as TermEvent, term::color::Colors as TermColors, tty, config::Config as TermConfig, event::Event as TermEvent, term::color::Colors as TermColors, tty,
}; };
use cosmic::{ use cosmic::{
app::{Command, Core, Settings}, app::{message, Command, Core, Settings},
cosmic_theme, executor, cosmic_theme, executor,
iced::{ iced::{
clipboard, event,
futures::SinkExt, futures::SinkExt,
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
subscription::{self, Subscription}, subscription::{self, Subscription},
widget::row, widget::row,
window, Alignment, Length, window, Alignment, Event, Length,
}, },
iced_core::Size, iced_core::Size,
style, style,
@ -58,6 +60,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
Copy,
Paste,
PasteValue(String),
TabActivate(segmented_button::Entity), TabActivate(segmented_button::Entity),
TabClose(segmented_button::Entity), TabClose(segmented_button::Entity),
TabNew, TabNew,
@ -118,6 +123,33 @@ impl cosmic::Application for App {
/// Handle application events here. /// Handle application events here.
fn update(&mut self, message: Self::Message) -> Command<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::Copy => {
if let Some(terminal) = self
.tab_model
.data::<Mutex<Terminal>>(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::<Mutex<Terminal>>(self.tab_model.active())
{
let terminal = terminal.lock().unwrap();
terminal.paste(value);
}
}
Message::TabActivate(entity) => { Message::TabActivate(entity) => {
self.tab_model.activate(entity); self.tab_model.activate(entity);
return self.update_title(); return self.update_title();
@ -279,26 +311,51 @@ impl cosmic::Application for App {
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {
struct TerminalEventWorker; struct TerminalEventWorker;
subscription::channel( Subscription::batch([
TypeId::of::<TerminalEventWorker>(), event::listen_with(|event, _status| match event {
100, Event::Keyboard(KeyEvent::KeyPressed {
|mut output| async move { key_code: KeyCode::C,
let (event_tx, mut event_rx) = mpsc::channel(100); modifiers,
output.send(Message::TermEventTx(event_tx)).await.unwrap(); }) => {
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
// Create first terminal tab Some(Message::Copy)
output.send(Message::TabNew).await.unwrap(); } else {
None
while let Some((entity, event)) = event_rx.recv().await { }
output
.send(Message::TermEvent(entity, event))
.await
.unwrap();
} }
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::<TerminalEventWorker>(),
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");
},
),
])
} }
} }

View file

@ -8,6 +8,7 @@ use alacritty_terminal::{
term::{ term::{
cell::Flags, cell::Flags,
color::{Colors, Rgb}, color::{Colors, Rgb},
TermMode,
}, },
tty, Term, tty, Term,
}; };
@ -207,6 +208,27 @@ impl Terminal {
self.scroll(TerminalScroll::Bottom); 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) { pub fn resize(&mut self, width: u32, height: u32) {
if width != self.size.width || height != self.size.height { if width != self.size.width || height != self.size.height {
let instant = Instant::now(); let instant = Instant::now();

View file

@ -224,7 +224,7 @@ where
background_color.r() as f32 / 255.0, background_color.r() as f32 / 255.0,
background_color.g() as f32 / 255.0, background_color.g() as f32 / 255.0,
background_color.b() 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.logo(),
state.modifiers.control(), state.modifiers.control(),
state.modifiers.alt(), state.modifiers.alt(),
state.modifiers.shift(),
) { ) {
(true, _, _) => { (true, _, _, _) => {
// Ignore super // Ignore super
} }
(false, true, _) => { (false, true, _, false) => {
// Handle ctrl for control characters (Ctrl-A to Ctrl-Z) // Handle ctrl for control characters (Ctrl-A to Ctrl-Z)
if character.is_control() { if character.is_control() {
let mut buf = [0, 0, 0, 0]; let mut buf = [0, 0, 0, 0];
let str = character.encode_utf8(&mut buf); let str = character.encode_utf8(&mut buf);
terminal.input_scroll(str.as_bytes().to_vec()); 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() { if !character.is_control() {
// Handle alt for non-control characters // Handle alt for non-control characters
let mut buf = [0x1B, 0, 0, 0, 0]; let mut buf = [0x1B, 0, 0, 0, 0];
let str = character.encode_utf8(&mut buf[1..]); let str = character.encode_utf8(&mut buf[1..]);
terminal.input_scroll(str.as_bytes().to_vec()); terminal.input_scroll(str.as_bytes().to_vec());
status = Status::Captured;
} }
} }
(false, false, false) => { (false, false, false, _) => {
// Handle no modifiers for non-control characters // Handle no modifiers for non-control characters
if !character.is_control() { if !character.is_control() {
let mut buf = [0, 0, 0, 0]; let mut buf = [0, 0, 0, 0];
let str = character.encode_utf8(&mut buf); let str = character.encode_utf8(&mut buf);
terminal.input_scroll(str.as_bytes().to_vec()); terminal.input_scroll(str.as_bytes().to_vec());
status = Status::Captured;
} }
} }
} }
status = Status::Captured;
} }
Event::Mouse(MouseEvent::ButtonPressed(button)) => { Event::Mouse(MouseEvent::ButtonPressed(button)) => {
if let Some(p) = cursor_position.position_in(layout.bounds()) { if let Some(p) = cursor_position.position_in(layout.bounds()) {