Skip to content

Commit

Permalink
feat(ui): implement clipboard copying
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-olszewski committed Jul 10, 2024
1 parent d6470dc commit 2ff87ef
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/turborepo-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workspace = true

[dependencies]
atty = { workspace = true }
base64 = "0.22"
chrono = { workspace = true }
console = { workspace = true }
crossterm = "0.27.0"
Expand All @@ -30,4 +31,5 @@ tui-term = { workspace = true }
turbopath = { workspace = true }
turborepo-ci = { workspace = true }
turborepo-vt100 = { workspace = true }
which = { workspace = true }
winapi = "0.3.9"
14 changes: 14 additions & 0 deletions crates/turborepo-ui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ impl<W> App<W> {

Ok(())
}

pub fn copy_selection(&self) {
let task = self
.tasks
.get(&self.active_task())
.expect("active task should exist");
let Some(text) = task.copy_selection() else {
return;
};
super::copy_to_clipboard(&text);
}
}

impl<W: Write> App<W> {
Expand Down Expand Up @@ -518,6 +529,9 @@ fn update(
Event::Mouse(m) => {
app.handle_mouse(m)?;
}
Event::CopySelection => {
app.copy_selection();
}
}
Ok(None)
}
Expand Down
113 changes: 113 additions & 0 deletions crates/turborepo-ui/src/tui/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Inspired by https://github.com/pvolok/mprocs/blob/master/src/clipboard.rs
use std::process::Stdio;

use base64::Engine;
use which::which;

pub fn copy_to_clipboard(s: &str) {
match copy_impl(s, &PROVIDER) {
Ok(()) => (),
Err(err) => tracing::debug!("Unable to copy: {}", err.to_string()),
}
}

#[allow(dead_code)]
enum Provider {
OSC52,
Exec(&'static str, Vec<&'static str>),
#[cfg(windows)]
Win,
NoOp,
}

#[cfg(windows)]
fn detect_copy_provider() -> Provider {
Provider::Win
}

#[cfg(target_os = "macos")]
fn detect_copy_provider() -> Provider {
if let Some(provider) = check_prog("pbcopy", &[]) {
return provider;
}
Provider::OSC52
}

#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn detect_copy_provider() -> Provider {
// Wayland
if std::env::var("WAYLAND_DISPLAY").is_ok() {
if let Some(provider) = check_prog("wl-copy", &["--type", "text/plain"]) {
return provider;
}
}
// X11
if std::env::var("DISPLAY").is_ok() {
if let Some(provider) = check_prog("xclip", &["-i", "-selection", "clipboard"]) {
return provider;
}
if let Some(provider) = check_prog("xsel", &["-i", "-b"]) {
return provider;
}
}
// Termux
if let Some(provider) = check_prog("termux-clipboard-set", &[]) {
return provider;
}
// Tmux
if std::env::var("TMUX").is_ok() {
if let Some(provider) = check_prog("tmux", &["load-buffer", "-"]) {
return provider;
}
}

Provider::OSC52
}

#[allow(dead_code)]
fn check_prog(cmd: &'static str, args: &[&'static str]) -> Option<Provider> {
if which(cmd).is_ok() {
Some(Provider::Exec(cmd, args.to_vec()))
} else {
None
}
}

fn copy_impl(s: &str, provider: &Provider) -> std::io::Result<()> {
match provider {
Provider::OSC52 => {
let mut stdout = std::io::stdout().lock();
use std::io::Write;
write!(
&mut stdout,
"\x1b]52;;{}\x07",
base64::engine::general_purpose::STANDARD.encode(s)
)?;
}

Provider::Exec(prog, args) => {
let mut child = std::process::Command::new(prog)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
std::io::Write::write_all(&mut child.stdin.as_ref().unwrap(), s.as_bytes())?;
child.wait()?;
}

#[cfg(windows)]
Provider::Win => {
clipboard_win::set_clipboard_string(s).map_err(|e| anyhow::Error::msg(e.to_string()))?
}

Provider::NoOp => (),
};

Ok(())
}

lazy_static::lazy_static! {
static ref PROVIDER: Provider = detect_copy_provider();
}
1 change: 1 addition & 0 deletions crates/turborepo-ui/src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub enum Event {
tasks: Vec<String>,
},
Mouse(crossterm::event::MouseEvent),
CopySelection,
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
Expand Down
11 changes: 10 additions & 1 deletion crates/turborepo-ui/src/tui/input.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::time::Duration;

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use tracing::debug;

use super::{app::LayoutSections, event::Event, Error};

Expand All @@ -19,7 +20,9 @@ pub fn input(options: InputOptions) -> Result<Option<Event>, Error> {
// poll with 0 duration will only return true if event::read won't need to wait
// for input
if crossterm::event::poll(Duration::from_millis(0))? {
match crossterm::event::read()? {
let event = crossterm::event::read()?;
debug!("Received event: {event:?}");
match event {
crossterm::event::Event::Key(k) => Ok(translate_key_event(&focus, k)),
crossterm::event::Event::Mouse(m) => match m.kind {
crossterm::event::MouseEventKind::ScrollDown => Ok(Some(Event::ScrollDown)),
Expand All @@ -28,6 +31,9 @@ pub fn input(options: InputOptions) -> Result<Option<Event>, Error> {
| crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
Ok(Some(Event::Mouse(m)))
}
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Right) => {
Ok(Some(Event::CopySelection))
}
_ => Ok(None),
},
_ => Ok(None),
Expand All @@ -50,6 +56,9 @@ fn translate_key_event(interact: &LayoutSections, key_event: KeyEvent) -> Option
KeyCode::Char('c') if key_event.modifiers == crossterm::event::KeyModifiers::CONTROL => {
ctrl_c()
}
KeyCode::Char('c') if key_event.modifiers == crossterm::event::KeyModifiers::SUPER => {
return Some(Event::CopySelection);
}
// Interactive branches
KeyCode::Char('z')
if matches!(interact, LayoutSections::Pane)
Expand Down
2 changes: 2 additions & 0 deletions crates/turborepo-ui/src/tui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod app;
mod clipboard;
pub mod event;
mod handle;
mod input;
Expand All @@ -9,6 +10,7 @@ mod task;
mod term_output;

pub use app::{run_app, terminal_big_enough};
use clipboard::copy_to_clipboard;
use event::{Event, TaskResult};
pub use handle::{AppReceiver, AppSender, TuiTask};
use input::{input, InputOptions};
Expand Down
4 changes: 4 additions & 0 deletions crates/turborepo-ui/src/tui/term_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,8 @@ impl<W> TerminalOutput<W> {
}
Ok(())
}

pub fn copy_selection(&self) -> Option<String> {
self.parser.screen().selected_text()
}
}

0 comments on commit 2ff87ef

Please sign in to comment.