From ae470bf68bf27921109890da3d90a5b61fa6a7aa Mon Sep 17 00:00:00 2001 From: Joe Wilm Date: Mon, 26 Dec 2016 22:52:37 -0500 Subject: [PATCH] Implement copying selection for macOS Still need automatic loading into selection copy buffer for linux. --- alacritty.yml | 1 + src/config.rs | 1 + src/event.rs | 1 - src/input.rs | 11 +++++-- src/selection.rs | 41 ++++++++++++++++++++++-- src/term/cell.rs | 43 +++++++++++++++++++++++-- src/term/mod.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 172 insertions(+), 8 deletions(-) diff --git a/alacritty.yml b/alacritty.yml index 61134552..1fbcb63f 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -116,6 +116,7 @@ colors: # bytes. Possible values of `action` include `Paste` and `PasteSelection`. key_bindings: - { key: V, mods: Command, action: Paste } + - { key: C, mods: Command, action: Copy } - { key: Home, chars: "\x1b[H", mode: ~AppCursor } - { key: Home, chars: "\x1b[1~", mode: AppCursor } - { key: End, chars: "\x1b[F", mode: ~AppCursor } diff --git a/src/config.rs b/src/config.rs index 9bdcc6e6..abf0bdf5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -280,6 +280,7 @@ impl de::Deserialize for ActionWrapper { { Ok(ActionWrapper(match value { "Paste" => Action::Paste, + "Copy" => Action::Copy, "PasteSelection" => Action::PasteSelection, _ => return Err(E::invalid_value("invalid value for Action")), })) diff --git a/src/event.rs b/src/event.rs index 0c4c701b..a231e898 100644 --- a/src/event.rs +++ b/src/event.rs @@ -148,7 +148,6 @@ impl Processor { }, glutin::Event::KeyboardInput(state, _code, key, mods, string) => { processor.process_key(state, key, mods, string); - processor.ctx.selection.clear(); }, glutin::Event::MouseInput(state, button) => { processor.mouse_input(state, button); diff --git a/src/input.rs b/src/input.rs index 92379be9..4339b69f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -148,8 +148,14 @@ impl Action { ctx.notifier.notify(s.clone().into_bytes()) }, Action::Copy => { - // so... need access to terminal state. and the selection. - unimplemented!(); + if let Some(selection) = ctx.selection.span() { + let buf = ctx.terminal.string_from_selection(&selection); + + Clipboard::new() + .expect("get clipboard") + .store_primary(buf) + .expect("copy into clipboard"); + } }, Action::Paste | Action::PasteSelection => { @@ -328,6 +334,7 @@ impl<'a, N: Notify + 'a> Processor<'a, N> { // Didn't process a binding; print the provided character if let Some(string) = string { self.ctx.notifier.notify(string.into_bytes()); + self.ctx.selection.clear(); } } } diff --git a/src/selection.rs b/src/selection.rs index 6c927967..a02d94e4 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -21,7 +21,7 @@ use std::mem; use std::ops::RangeInclusive; -use index::{Location, Column, Side, Linear}; +use index::{Location, Column, Side, Linear, Line}; use grid::ToRange; /// The area selected @@ -94,7 +94,7 @@ impl Selection { pub fn span(&self) -> Option { match *self { - Selection::Active {ref start, ref end, ref start_side, ref end_side } => { + Selection::Active { ref start, ref end, ref start_side, ref end_side } => { let (front, tail, front_side, tail_side) = if *start > *end { // Selected upward; start/end are swapped (end, start, end_side, start_side) @@ -180,6 +180,43 @@ pub struct Span { } impl Span { + pub fn to_locations(&self, cols: Column) -> (Location, Location) { + match self.ty { + SpanType::Inclusive => (self.front, self.tail), + SpanType::Exclusive => { + (Span::wrap_start(self.front, cols), Span::wrap_end(self.tail, cols)) + }, + SpanType::ExcludeFront => (Span::wrap_start(self.front, cols), self.tail), + SpanType::ExcludeTail => (self.front, Span::wrap_end(self.tail, cols)) + } + } + + fn wrap_start(mut start: Location, cols: Column) -> Location { + if start.col == cols - 1 { + Location { + line: start.line + 1, + col: Column(0), + } + } else { + start.col += 1; + start + } + } + + fn wrap_end(end: Location, cols: Column) -> Location { + if end.col == Column(0) && end.line != Line(0) { + Location { + line: end.line - 1, + col: cols + } + } else { + Location { + line: end.line, + col: end.col - 1 + } + } + } + #[inline] fn exclude_start(start: Linear) -> Linear { start + 1 diff --git a/src/term/cell.rs b/src/term/cell.rs index df648294..a95875bf 100644 --- a/src/term/cell.rs +++ b/src/term/cell.rs @@ -11,11 +11,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - - use std::mem; use ansi::{NamedColor, Color}; +use grid; +use index::Column; bitflags! { #[derive(Serialize, Deserialize)] @@ -35,6 +35,27 @@ pub struct Cell { pub flags: Flags, } +/// Get the length of occupied cells in a line +pub trait LineLength { + /// Calculate the occupied line length + fn line_length(&self) -> Column; +} + +impl LineLength for grid::Row { + fn line_length(&self) -> Column { + let mut length = Column(0); + + for (index, cell) in self[..].iter().rev().enumerate() { + if cell.c != ' ' { + length = Column(self.len() - index); + break; + } + } + + length + } +} + impl Cell { pub fn bold(&self) -> bool { self.flags.contains(BOLD) @@ -67,3 +88,21 @@ impl Cell { mem::swap(&mut self.fg, &mut self.bg); } } + +#[cfg(test)] +mod tests { + use super::{Cell, LineLength}; + + use grid::Row; + use index::Column; + use ansi::Color; + + #[test] + fn line_length_works() { + let template = Cell::new(' ', Color::Indexed(0), Color::Indexed(0)); + let mut row = Row::new(Column(10), &template); + row[Column(5)].c = 'a'; + + assert_eq!(row.line_length(), Column(6)); + } +} diff --git a/src/term/mod.rs b/src/term/mod.rs index b25e8382..28e73b19 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -21,10 +21,11 @@ use std::io; use ansi::{self, Color, NamedColor, Attr, Handler}; use grid::{Grid, ClearRegion, ToRange}; use index::{self, Cursor, Column, Line, Linear}; -use selection::Selection; +use selection::{Span, Selection}; pub mod cell; pub use self::cell::Cell; +use self::cell::LineLength; /// Iterator that yields cells needing render /// @@ -309,6 +310,85 @@ impl Term { } } + pub fn string_from_selection(&self, span: &Span) -> String { + trait Append { + fn append(&mut self, grid: &Grid, line: Line, cols: T); + } + + use std::ops::{Range, RangeTo, RangeFrom, RangeFull}; + + impl Append> for String { + fn append(&mut self, grid: &Grid, line: Line, cols: Range) { + let line = &grid[line]; + let line_length = line.line_length(); + let line_end = cmp::min(line_length, cols.end + 1); + for cell in &line[cols.start..line_end] { + self.push(cell.c); + } + } + } + + impl Append> for String { + #[inline] + fn append(&mut self, grid: &Grid, line: Line, cols: RangeTo) { + self.append(grid, line, Column(0)..cols.end); + } + } + + impl Append> for String { + #[inline] + fn append(&mut self, grid: &Grid, line: Line, cols: RangeFrom) { + self.append(grid, line, cols.start..Column(usize::max_value() - 1)); + self.push('\n'); + } + } + + impl Append for String { + #[inline] + fn append(&mut self, grid: &Grid, line: Line, _: RangeFull) { + self.append(grid, line, Column(0)..Column(usize::max_value() - 1)); + self.push('\n'); + } + } + + let mut res = String::new(); + + let (start, end) = span.to_locations(self.grid.num_cols()); + let line_count = end.line - start.line; + + match line_count { + // Selection within single line + Line(0) => { + res.append(&self.grid, start.line, start.col..end.col); + }, + + // Selection ends on line following start + Line(1) => { + // Starting line + res.append(&self.grid, start.line, start.col..); + + // Ending line + res.append(&self.grid, end.line, ..end.col); + }, + + // Multi line selection + _ => { + // Starting line + res.append(&self.grid, start.line, start.col..); + + let middle_range = (start.line + 1)..(end.line); + for line in middle_range { + res.append(&self.grid, line, ..); + } + + // Ending line + res.append(&self.grid, end.line, ..end.col); + } + } + + res + } + /// Convert the given pixel values to a grid coordinate /// /// The mouse coordinates are expected to be relative to the top left. The