Skip to content

Commit d360e75

Browse files
committed
Move clipboard to core
1 parent d33ae64 commit d360e75

10 files changed

Lines changed: 81 additions & 55 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gravel-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ edition.workspace = true
77
gravel-ffi = { path = "../gravel-ffi" }
88

99
abi_stable.workspace = true
10+
arboard.workspace = true
1011
enumflags2.workspace = true
1112
fuzzy-matcher.workspace = true
1213
hotkey.workspace = true

gravel-core/src/clipboard.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std::sync::Mutex;
2+
3+
pub struct Clipboard {
4+
inner: Option<Mutex<arboard::Clipboard>>,
5+
}
6+
7+
impl Clipboard {
8+
#[expect(clippy::new_without_default)]
9+
pub fn new() -> Self {
10+
Self {
11+
inner: create_clipboard().map(Mutex::new),
12+
}
13+
}
14+
15+
pub fn set_text(&self, content: &str) {
16+
log::trace!("setting clipboard to {content}");
17+
18+
let Some(mutex) = &self.inner else {
19+
log::trace!("clipboard not initialized, canceling operation");
20+
return;
21+
};
22+
23+
let Ok(mut clipboard) = mutex.lock() else {
24+
log::error!("clipboard mutex poisened, canceling operation");
25+
return;
26+
};
27+
28+
clipboard
29+
.set_text(content)
30+
.inspect_err(|e| log::error!("unable to set clipboard: {e}"))
31+
.ok();
32+
}
33+
}
34+
35+
fn create_clipboard() -> Option<arboard::Clipboard> {
36+
log::trace!("spawning clipboard instance");
37+
38+
arboard::Clipboard::new()
39+
.inspect_err(|e| log::error!("unable to initialize clipboard: {e}"))
40+
.ok()
41+
}

gravel-core/src/engine.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ impl HitActionContext for ActionContext {
178178
self.send_frontend(FrontendMessage::ShowWithQuery(query));
179179
}
180180

181+
fn set_clipboard_text(&self, content: RString) {
182+
self.send(CoreMessage::SetClipboardText(content.into_rust()));
183+
}
184+
181185
fn clear_caches(&self) {
182186
self.send(CoreMessage::ClearCaches);
183187
}

gravel-core/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use gravel_ffi::{BoxDynFrontendContext, FrontendContext, FrontendMessage, Fronte
1313
use std::sync::atomic::{AtomicU32, Ordering};
1414
use std::{thread, time::Duration};
1515

16+
use crate::clipboard::Clipboard;
17+
18+
pub mod clipboard;
1619
pub mod config;
1720
pub mod engine;
1821
pub mod hotkeys;
@@ -25,13 +28,15 @@ pub struct Core {
2528
engine: QueryEngine,
2629
frontend_sender: RSender<FrontendMessageNe>,
2730
receiver: RReceiver<CoreMessage>,
31+
clipboard: Clipboard,
2832
}
2933

3034
pub enum CoreMessage {
3135
Frontend(FrontendMessage),
3236
Query(u32, String),
3337
RunAction(ArcDynHit, ActionKind),
3438
ClearCaches,
39+
SetClipboardText(String),
3540
}
3641

3742
impl Core {
@@ -44,6 +49,7 @@ impl Core {
4449
engine,
4550
frontend_sender,
4651
receiver,
52+
clipboard: Clipboard::new(),
4753
}
4854
}
4955

@@ -65,6 +71,7 @@ impl Core {
6571
CoreMessage::Query(token, query) => self.query(token, &query),
6672
CoreMessage::RunAction(hit, kind) => self.run_action(&hit, kind),
6773
CoreMessage::ClearCaches => self.clear_caches(),
74+
CoreMessage::SetClipboardText(content) => self.clipboard.set_text(&content),
6875
}
6976

7077
None

gravel-ffi/src/hit.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ pub trait HitActionContext {
104104

105105
/// Clears caches in the entire application.
106106
fn clear_caches(&self);
107+
108+
/// Writes to the system clipboard.
109+
fn set_clipboard_text(&self, content: RString);
107110
}
108111

109112
type SimpleHitAction = Box<dyn Fn(&SimpleHit, RefDynHitActionContext<'_>) + Send + Sync>;

gravel-ffi/src/provider.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ pub trait ProviderInner: Send {
3636
/// }
3737
///
3838
/// fn query(&self, query: &str) -> ProviderResult {
39-
///
4039
/// ProviderResult::empty()
4140
/// }
4241
/// }
@@ -76,6 +75,12 @@ impl ProviderResult {
7675
Self { hits }
7776
}
7877

78+
/// Constructs a new [`ProviderResult`] with one or no hits.
79+
#[must_use]
80+
pub fn from_option(hit: Option<impl Into<ArcDynHit>>) -> Self {
81+
hit.map_or_else(Self::empty, Self::single)
82+
}
83+
7984
/// Constructs an empty [`ProviderResult`].
8085
#[must_use]
8186
pub fn empty() -> Self {

gravel-provider-calculator/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ no-root = []
1313
gravel-ffi = { path = "../gravel-ffi" }
1414

1515
abi_stable.workspace = true
16-
arboard.workspace = true
17-
log.workspace = true
1816
mexprp.workspace = true
1917
serde.workspace = true
2018

gravel-provider-calculator/src/lib.rs

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,51 @@
55
//!
66
//! Selecting the hit copies the calculated value to the system's clipboard.
77
8-
use arboard::Clipboard;
8+
use abi_stable::reexports::SelfOps;
99
use gravel_ffi::prelude::*;
1010
use mexprp::Answer;
1111
use serde::Deserialize;
12-
use std::cell::OnceCell;
13-
use std::sync::{Arc, Mutex};
1412

1513
const DEFAULT_CONFIG: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/config.yml"));
1614

1715
struct CalculatorProvider {
1816
config: Config,
19-
clipboard: OnceCell<Option<Arc<Mutex<Clipboard>>>>,
20-
}
21-
22-
impl CalculatorProvider {
23-
fn get_clipboard(&self) -> Option<Arc<Mutex<Clipboard>>> {
24-
self.clipboard.get_or_init(create_clipboard).clone()
25-
}
2617
}
2718

2819
#[gravel_provider("calculator")]
2920
impl Provider for CalculatorProvider {
3021
fn new(config: &PluginConfigAdapter<'_>) -> Self {
3122
Self {
3223
config: config.get(DEFAULT_CONFIG),
33-
clipboard: OnceCell::new(),
3424
}
3525
}
3626

3727
fn query(&self, query: &str) -> ProviderResult {
3828
let query = query.trim();
39-
let result = eval(query);
40-
41-
let Some(result) = result else {
42-
return ProviderResult::empty();
43-
};
4429

45-
if query == result || matches!(query, "e" | "pi" | "i") {
46-
return ProviderResult::empty();
47-
}
48-
49-
let clipboard = self.get_clipboard();
30+
eval(query)
31+
.filter(|r| !query_was_const(query, r))
32+
.map(|r| self.get_hit(r))
33+
.piped(ProviderResult::from_option)
34+
}
35+
}
5036

51-
let hit = SimpleHit::new(result, self.config.subtitle.clone(), move |hit, ctx| {
52-
do_copy(clipboard.clone(), hit.title().as_str(), ctx);
37+
impl CalculatorProvider {
38+
fn get_hit(&self, result: String) -> SimpleHit {
39+
SimpleHit::new(result, self.config.subtitle.clone(), move |hit, ctx| {
40+
ctx.set_clipboard_text(hit.title().to_string().into_c());
41+
ctx.hide_frontend();
5342
})
5443
.with_secondary(|hit, ctx| {
55-
ctx.set_query(hit.title().into_rust().to_owned().into_c());
44+
ctx.set_query(hit.title().as_str().to_owned().into_c());
5645
})
57-
.with_score(MAX_SCORE);
58-
59-
ProviderResult::single(hit)
46+
.with_score(MAX_SCORE)
6047
}
6148
}
6249

63-
fn create_clipboard() -> Option<Arc<Mutex<Clipboard>>> {
64-
log::trace!("spawning clipboard instance");
65-
66-
Clipboard::new()
67-
.inspect_err(|e| log::error!("unable to initialize clipboard: {e}"))
68-
.ok()
69-
.map(|c| Arc::new(Mutex::new(c)))
50+
// queries that do not require any calculation should be ignored
51+
fn query_was_const(query: &str, result: &str) -> bool {
52+
query == result || matches!(query, "e" | "pi" | "i")
7053
}
7154

7255
fn eval(expression: &str) -> Option<String> {
@@ -78,22 +61,6 @@ fn eval(expression: &str) -> Option<String> {
7861
.map(|r| round(r, 10).to_string())
7962
}
8063

81-
fn do_copy(clipboard: Option<Arc<Mutex<Clipboard>>>, result: &str, context: RefDynHitActionContext<'_>) {
82-
let Some(clipboard_mutex) = clipboard else {
83-
return;
84-
};
85-
86-
log::debug!("copying value to clipboard: {result}");
87-
88-
let mut guard = clipboard_mutex.lock().expect("thread holding the mutex can't panic");
89-
guard
90-
.set_text(result)
91-
.inspect_err(|e| log::error!("unable to set clipboard: {e}"))
92-
.ok();
93-
94-
context.hide_frontend();
95-
}
96-
9764
fn round(number: f64, precision: u32) -> f64 {
9865
let factor = 10_u64.pow(precision) as f64;
9966
(number * factor).round() / factor

gravel-test-utils/src/mock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mock! {
1010
fn exit(&self);
1111
fn restart(&self);
1212
fn set_query(&self, query: RString);
13+
fn set_clipboard_text(&self, content: RString);
1314
fn clear_caches(&self);
1415
}
1516
}

0 commit comments

Comments
 (0)