Skip to content

Copy to clipboard #590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
440 changes: 368 additions & 72 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions numbat-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ toml = { version = "0.8.8", features = ["parse"] }
serde = { version = "1.0.195", features = ["derive"] }
terminal_size = "0.3.0"
jiff = "0.1"
arboard = "3.4.1"

[dependencies.clap]
version = "4"
Expand Down
4 changes: 4 additions & 0 deletions numbat-cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::copy_to_clipboard::NumericDisplayConfig;
use clap::ValueEnum;
use numbat::compact_str::CompactString;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -66,6 +67,8 @@ pub struct Config {
#[serde(skip_serializing)]
pub load_user_init: bool,
pub exchange_rates: ExchangeRateConfig,

pub copy_result: NumericDisplayConfig,
}

impl Default for Config {
Expand All @@ -79,6 +82,7 @@ impl Default for Config {
load_user_init: true,
exchange_rates: Default::default(),
enter_repl: true,
copy_result: Default::default(),
}
}
}
170 changes: 170 additions & 0 deletions numbat-cli/src/copy_to_clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use numbat::{
markup::Markup, num_format::CustomFormat, num_format::Grouping, pretty_dtoa::FmtFloatConfig,
pretty_print::PrettyPrint, value::Value, FloatDisplayConfigSource, RuntimeError,
UnitDisplayOptions,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Eq, Default, Debug, Clone)]
#[serde(remote = "Grouping", rename_all = "kebab-case")]
pub enum IntGroupingConfig {
#[default]
Standard,
Indian,
#[serde(rename = "none")]
Posix,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct IntDisplayConfig {
#[serde(with = "IntGroupingConfig")]
pub grouping: Grouping,
pub separator: String,
pub minus_sign: String,
}

impl Default for IntDisplayConfig {
fn default() -> Self {
Self {
grouping: Grouping::Standard,
separator: ",".into(),
minus_sign: "-".into(),
}
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FloatDisplayConfig {
pub decimal: char,
pub capitalize_e: bool,
}

impl Default for FloatDisplayConfig {
fn default() -> Self {
Self {
decimal: '.',
capitalize_e: false,
}
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct UnitDisplayConfig {
fancy_exponents: bool,
multiplication_operator: char,
division_operator: char,
space_btwn_operators: bool,
}

impl Default for UnitDisplayConfig {
fn default() -> Self {
Self {
fancy_exponents: false,
multiplication_operator: '·',
division_operator: '/',
space_btwn_operators: false,
}
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Default, Debug, Clone)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct NumericDisplayConfig {
#[serde(serialize_with = "serialize_option_config")]
pub int_config: Option<IntDisplayConfig>,
#[serde(serialize_with = "serialize_option_config")]
pub float_config: Option<FloatDisplayConfig>,
#[serde(serialize_with = "serialize_option_config")]
pub unit_config: Option<UnitDisplayConfig>,
}

fn serialize_option_config<S, T: Serialize + Default>(
value: &Option<T>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(o) => o.serialize(serializer),
None => T::default().serialize(serializer),
}
}

pub(crate) fn pretty_print_value(
v: &Value,
config: &NumericDisplayConfig,
) -> Result<Markup, Box<RuntimeError>> {
Ok(match &v {
Value::Quantity(q) => {
let NumericDisplayConfig {
int_config,
float_config,
unit_config,
} = config;

let float_options = float_config.as_ref().map(|c| {
let FloatDisplayConfig {
decimal,
capitalize_e,
} = *c;

FmtFloatConfig::default()
.radix_point(decimal)
.capitalize_e(capitalize_e)
});

let int_options = int_config
.as_ref()
.map(|c| {
let int_config @ IntDisplayConfig {
grouping,
separator,
minus_sign,
} = c;

CustomFormat::builder()
.grouping(*grouping)
.separator(separator)
.minus_sign(minus_sign)
.build()
.map_err(|err| {
Box::new(RuntimeError::IntegerDisplayConfig(
format!("{int_config:?}"),
err.to_string(),
))
})
})
.transpose()?;

let unit_options = unit_config.as_ref().map(|c| {
let &UnitDisplayConfig {
fancy_exponents,
multiplication_operator,
division_operator,
space_btwn_operators,
} = c;
UnitDisplayOptions {
fancy_exponents,
multiplication_operator,
division_operator,
space_btwn_operators,
}
});

q.pretty_print_with_options(
match float_options {
Some(o) => FloatDisplayConfigSource::UserConfig(o),
None => FloatDisplayConfigSource::Numbat(None),
},
int_options,
unit_options,
)
}

v => v.pretty_print(),
})
}
110 changes: 79 additions & 31 deletions numbat-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
mod ansi_formatter;
mod completer;
mod config;
mod copy_to_clipboard;
mod highlighter;

use ansi_formatter::ansi_format;
use colored::control::SHOULD_COLORIZE;
use completer::NumbatCompleter;
use config::{ColorMode, Config, ExchangeRateFetchingPolicy, IntroBanner, PrettyPrintMode};
use copy_to_clipboard::pretty_print_value;
use highlighter::NumbatHighlighter;

use itertools::Itertools;
use numbat::command::{self, CommandParser, SourcelessCommandParser};
use numbat::compact_str::{CompactString, ToCompactString};
use numbat::diagnostic::ErrorDiagnostic;
use numbat::help::help_markup;
use numbat::markup as m;
use numbat::markup::{Formatter, PlainTextFormatter};
use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter};
use numbat::pretty_print::PrettyPrint;
use numbat::resolver::CodeSource;
use numbat::session_history::{ParseEvaluationResult, SessionHistory, SessionHistoryOptions};
use numbat::session_history::{SessionHistory, SessionHistoryOptions};
use numbat::value::Value;
use numbat::{markup as m, InterpreterResult};
use numbat::{Context, NumbatError};
use numbat::{InterpreterSettings, NameResolutionError};
use numbat::{InterpreterSettings, RuntimeError};

use anyhow::{bail, Context as AnyhowContext, Result};
use clap::Parser;
Expand Down Expand Up @@ -98,7 +102,7 @@ struct Args {

struct ParseEvaluationOutcome {
control_flow: ControlFlow,
result: ParseEvaluationResult,
result: Result<InterpreterResult, ()>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -352,9 +356,21 @@ impl Cli {
interactive: bool,
) -> Result<()> {
let mut session_history = SessionHistory::default();
let mut last_value = None::<Value>;
let mut clipboard = match arboard::Clipboard::new() {
Ok(cb) => Some(cb),
Err(_) => {
println!(
"error: could not initialize the clipboard, so
`copy` functionality will be disabled this session"
);
None
}
};

loop {
let readline = rl.readline(&self.config.prompt);

match readline {
Ok(line) => {
if !line.trim().is_empty() {
Expand Down Expand Up @@ -408,6 +424,44 @@ impl Cli {
};
println!("{}", ansi_format(&m, false));
}
command::Command::Copy => {
let Some(clipboard) = &mut clipboard else {
println!(
"error: as the clipboard could not \
be initialized, `copy` functionality is \
disabled for this session"
);
continue;
};
match &last_value {
Some(v) => {
let m = match pretty_print_value(
v,
&self.config.copy_result,
) {
Ok(m) => m,
Err(err) => {
self.print_diagnostic(*err);
continue;
}
};

let text = PlainTextFormatter.format(&m, false);
if let Err(e) = clipboard.set_text(text) {
self.print_diagnostic(
RuntimeError::ClipboardError(e.to_string()),
);
continue;
}

println!(
"{} was copied to the clipboard",
ansi_format(&m, false)
);
}
None => println!("error: no value to copy"),
}
}
command::Command::Clear => rl.clear_screen()?,
command::Command::Save { dst } => {
let save_result = session_history.save(
Expand Down Expand Up @@ -466,7 +520,15 @@ impl Cli {
}
}

session_history.push(CompactString::from(line), result);
match result {
Ok(result) => {
session_history.push(CompactString::from(line), Ok(()));
if let InterpreterResult::Value(value) = result {
last_value = Some(value);
}
}
Err(_) => session_history.push(CompactString::from(line), Err(())),
}
}
}
Err(ReadlineError::Interrupted) => {}
Expand Down Expand Up @@ -510,12 +572,7 @@ impl Cli {
PrettyPrintMode::Auto => interactive,
};

let parse_eval_result = match &interpretation_result {
Ok(_) => Ok(()),
Err(_) => Err(()),
};

let control_flow = match interpretation_result.map_err(|b| *b) {
let (control_flow, result) = match interpretation_result.map_err(|b| *b) {
Ok((statements, interpreter_result)) => {
if interactive || pretty_print {
println!();
Expand Down Expand Up @@ -551,32 +608,23 @@ impl Cli {
println!();
}

ControlFlow::Continue(())
}
Err(NumbatError::ResolverError(e)) => {
self.print_diagnostic(e);
execution_mode.exit_status_in_case_of_error()
(ControlFlow::Continue(()), Ok(interpreter_result))
}
Err(NumbatError::NameResolutionError(
e @ (NameResolutionError::IdentifierClash { .. }
| NameResolutionError::ReservedIdentifier(_)),
)) => {
self.print_diagnostic(e);
execution_mode.exit_status_in_case_of_error()
}
Err(NumbatError::TypeCheckError(e)) => {
self.print_diagnostic(e);
execution_mode.exit_status_in_case_of_error()
}
Err(NumbatError::RuntimeError(e)) => {
self.print_diagnostic(e);
execution_mode.exit_status_in_case_of_error()
Err(err) => {
match err {
NumbatError::ResolverError(e) => self.print_diagnostic(e),
NumbatError::NameResolutionError(e) => self.print_diagnostic(e),
NumbatError::TypeCheckError(e) => self.print_diagnostic(e),
NumbatError::RuntimeError(e) => self.print_diagnostic(e),
}

(execution_mode.exit_status_in_case_of_error(), Err(()))
}
};

ParseEvaluationOutcome {
control_flow,
result: parse_eval_result,
result,
}
}

Expand Down
Loading
Loading