wip: copy/paste from system clipboard

This commit is contained in:
Jeremy Wall 2025-04-10 21:19:09 -04:00
parent 563a885b70
commit 8a76a031cb
5 changed files with 63 additions and 12 deletions

View File

@ -18,6 +18,7 @@ The currently supported commands are:
* `help [topic]` Display help for a given topic. * `help [topic]` Display help for a given topic.
* `export-csv <path>` Export the current sheet to a csv file at `<path>`. * `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. * `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 <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 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 <aside>Note that for `q` this will not currently prompt you if the sheet is not
saved.</aside> 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. * `h`, `j`, `k`, `l` will navigate around the sheet.
* `Ctrl-n`, `Ctrl-p` will navigate between sheets. * `Ctrl-n`, `Ctrl-p` will navigate between sheets.
* `Ctrl-c`, `y` Copy the cell or range contents. * `Ctrl-c`, `y` Copy the cell or range formatted contents.
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content. * `Ctrl-Shift-C`, 'Y' Copy the cell or range content.
* `The spacebar will select the start and end of the range respectively. * `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 leaving any style untouched
* `D` will delete the contents of the range including any style * `D` will delete the contents of the range including any style

View File

@ -16,6 +16,7 @@ pub enum Cmd<'a> {
Edit(&'a str), Edit(&'a str),
Help(Option<&'a str>), Help(Option<&'a str>),
ExportCsv(&'a str), ExportCsv(&'a str),
SystemPaste,
Quit, 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())? { if let Some(cmd) = try_consume_color_cell(cursor.clone())? {
return Ok(Some(cmd)); return Ok(Some(cmd));
} }
if let Some(cmd) = try_consume_system_paste(cursor.clone())? {
return Ok(Some(cmd));
}
Ok(None) Ok(None)
} }
@ -316,6 +320,22 @@ fn try_consume_quit<'cmd, 'i: 'cmd>(
return Ok(Some(Cmd::Quit)); 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>( fn try_consume_rename_sheet<'cmd, 'i: 'cmd>(
mut input: StrCursor<'i>, mut input: StrCursor<'i>,
) -> Result<Option<Cmd<'cmd>>, &'static str> { ) -> Result<Option<Cmd<'cmd>>, &'static str> {

View File

@ -513,6 +513,12 @@ impl<'ws> Workspace<'ws> {
.set_cell_style(&[("fill.bg_color", &color)], &area)?; .set_cell_style(&[("fill.bg_color", &color)], &area)?;
Ok(None) 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) => { Ok(None) => {
self.enter_dialog_mode(Markdown::from_str(&format!( self.enter_dialog_mode(Markdown::from_str(&format!(
"Unrecognized commmand {}", "Unrecognized commmand {}",
@ -607,19 +613,19 @@ impl<'ws> Workspace<'ws> {
Ok(()) Ok(())
})?; })?;
} }
KeyCode::Char('C') if key.modifiers.contains(KeyModifiers::CONTROL) => { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.copy_range(true)?; self.copy_range(true)?;
self.exit_range_select_mode()?; 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') => { 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.copy_range(false)?;
self.exit_range_select_mode()?; self.exit_range_select_mode()?;
} }
@ -662,7 +668,9 @@ impl<'ws> Workspace<'ws> {
} }
// TODO(zaphar): Rethink this a bit perhaps? // TODO(zaphar): Rethink this a bit perhaps?
let mut cb = Clipboard::new()?; 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))?; cb.set_html(html, Some(csv))?;
self.state.clipboard = Some(ClipboardContents::Range(rows)); self.state.clipboard = Some(ClipboardContents::Range(rows));
} }
@ -677,6 +685,26 @@ impl<'ws> Workspace<'ws> {
Ok(()) 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> { fn update_range_selection(&mut self) -> Result<bool, anyhow::Error> {
Ok(if self.state.range_select.start.is_none() { Ok(if self.state.range_select.start.is_none() {
self.state.range_select.start = Some(self.book.location.clone()); self.state.range_select.start = Some(self.book.location.clone());