Compare commits

...

2 Commits

5 changed files with 97 additions and 13 deletions

View File

@ -18,6 +18,7 @@ The currently supported commands are:
* `help [topic]` Display help for a given topic.
* `export-csv <path>` Export the current sheet to a csv file at `<path>`.
* `quit` Quits the application. `q` is a shorthand alias for this command.
* `system-paste` Paste from the system clipboard
<aside>Note that in the case of `quit` and `edit` that we do not currently
prompt you if the current spreadsheet has not been saved yet. So your changes

View File

@ -49,3 +49,5 @@ Range selections made from navigation mode will be available to paste into a Cel
<aside>Note that for `q` this will not currently prompt you if the sheet is not
saved.</aside>
Note also that copy paste works with the system clipboard.

View File

@ -5,8 +5,8 @@ select mode from CellEdit mode with `CTRL-r`.
* `h`, `j`, `k`, `l` will navigate around the sheet.
* `Ctrl-n`, `Ctrl-p` will navigate between sheets.
* `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content.
* `Ctrl-c`, `y` Copy the cell or range formatted contents.
* `Ctrl-Shift-C`, 'Y' Copy the cell or range content.
* `The spacebar will select the start and end of the range respectively.
* `d` will delete the contents of the range leaving any style untouched
* `D` will delete the contents of the range including any style

View File

@ -16,6 +16,7 @@ pub enum Cmd<'a> {
Edit(&'a str),
Help(Option<&'a str>),
ExportCsv(&'a str),
SystemPaste,
Quit,
}
@ -68,6 +69,9 @@ pub fn parse<'cmd, 'i: 'cmd>(input: &'i str) -> Result<Option<Cmd<'cmd>>, &'stat
if let Some(cmd) = try_consume_color_cell(cursor.clone())? {
return Ok(Some(cmd));
}
if let Some(cmd) = try_consume_system_paste(cursor.clone())? {
return Ok(Some(cmd));
}
Ok(None)
}
@ -316,6 +320,22 @@ fn try_consume_quit<'cmd, 'i: 'cmd>(
return Ok(Some(Cmd::Quit));
}
fn try_consume_system_paste<'cmd, 'i: 'cmd>(
mut input: StrCursor<'i>,
) -> Result<Option<Cmd<'cmd>>, &'static str> {
const LONG: &'static str = "system-paste";
if compare(input.clone(), LONG) {
input.seek(LONG.len());
} else {
return Ok(None);
}
if input.remaining() > 0 {
return Err("Invalid command: system-paste does not take an argument");
}
return Ok(Some(Cmd::SystemPaste));
}
fn try_consume_rename_sheet<'cmd, 'i: 'cmd>(
mut input: StrCursor<'i>,
) -> Result<Option<Cmd<'cmd>>, &'static str> {

View File

@ -5,6 +5,7 @@ use crate::book::{self, AddressRange, Book};
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use csv::StringRecord;
use ironcalc::base::{expressions::types::Area, Model};
use ratatui::{
buffer::Buffer,
@ -513,6 +514,12 @@ impl<'ws> Workspace<'ws> {
.set_cell_style(&[("fill.bg_color", &color)], &area)?;
Ok(None)
}
Ok(Some(Cmd::SystemPaste)) => {
let rows = self.get_rows_from_system_cipboard()?;
self.state.clipboard = Some(ClipboardContents::Range(rows));
self.paste_range()?;
Ok(None)
}
Ok(None) => {
self.enter_dialog_mode(Markdown::from_str(&format!(
"Unrecognized commmand {}",
@ -607,19 +614,19 @@ impl<'ws> Workspace<'ws> {
Ok(())
})?;
}
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.copy_range(true)?;
self.exit_range_select_mode()?;
}
KeyCode::Char('Y') => {
self.copy_range(true)?;
self.exit_range_select_mode()?;
}
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.copy_range(false)?;
self.exit_range_select_mode()?;
}
KeyCode::Char('y') => {
self.copy_range(true)?;
self.exit_range_select_mode()?;
}
KeyCode::Char('C') if key.modifiers == KeyModifiers::CONTROL => {
self.copy_range(false)?;
self.exit_range_select_mode()?;
}
KeyCode::Char('Y') => {
self.copy_range(false)?;
self.exit_range_select_mode()?;
}
@ -662,7 +669,9 @@ impl<'ws> Workspace<'ws> {
}
// TODO(zaphar): Rethink this a bit perhaps?
let mut cb = Clipboard::new()?;
let (html, csv) = self.book.range_to_clipboard_content(AddressRange { start, end })?;
let (html, csv) = self
.book
.range_to_clipboard_content(AddressRange { start, end })?;
cb.set_html(html, Some(csv))?;
self.state.clipboard = Some(ClipboardContents::Range(rows));
}
@ -677,6 +686,31 @@ impl<'ws> Workspace<'ws> {
Ok(())
}
fn get_rows_from_system_cipboard(&mut self) -> Result<Vec<Vec<String>>, anyhow::Error> {
use arboard::Clipboard;
let mut cb = Clipboard::new()?;
let txt = match cb.get_text() {
Ok(txt) => txt,
Err(e) => return Err(anyhow!(e)),
};
let reader = csv::Reader::from_reader(txt.as_bytes());
let mut rows = Vec::new();
for rec in reader.into_byte_records() {
let record = rec?;
let mut row = Vec::with_capacity(record.len());
for i in 0..record.len() {
row.push(
String::from_utf8_lossy(
record.get(i).expect("Unexpected failure to get cell row"),
)
.to_string(),
);
}
rows.push(row);
}
Ok(rows)
}
fn update_range_selection(&mut self) -> Result<bool, anyhow::Error> {
Ok(if self.state.range_select.start.is_none() {
self.state.range_select.start = Some(self.book.location.clone());
@ -944,7 +978,34 @@ impl<'ws> Workspace<'ws> {
self.book.evaluate();
}
None => {
// NOOP
// Try to get from system clipboard
use arboard::Clipboard;
let mut cb = Clipboard::new()?;
let csv = cb.get_text()?;
let rdr = csv::Reader::from_reader(csv.as_bytes());
let records: Vec<Result<csv::StringRecord, csv::Error>> =
rdr.into_records().collect();
let Address { sheet, row, col } = self.book.location.clone();
let row_len = records.len();
for ri in 0..row_len {
let columns = &records[ri];
if let Ok(columns) = columns {
let col_len = columns.len();
for ci in 0..col_len {
self.book.update_cell(
&Address {
sheet,
row: ri + row,
col: ci + col,
},
columns
.get(ci)
.expect("Failed to get column value from csv")
.to_string(),
)?;
}
}
}
}
}
self.state.clipboard = None;