Skip to content

Commit 1bb6f0f

Browse files
authored
Merge pull request #53 from unsecretised/clipboardhistory
Clipboardhistory
2 parents 6f64ba5 + 26f0c5a commit 1bb6f0f

5 files changed

Lines changed: 178 additions & 15 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ bit wonky, and will be fixed in the upcoming releases
5656

5757
- [ ] Select the options using arrow keys
5858
- [ ] Popup note-taking
59-
- [ ] Clipboard History
6059
- [ ] Plugin Support (Partially implemented on 15/12/2025)
6160
- [ ] Blur / transparent background (Partially implemented on 13/12/2025)
6261
- [ ] Hyperkey - Map CMD + OPT + CTRL + SHIFT to a physical key
@@ -79,6 +78,9 @@ bit wonky, and will be fixed in the upcoming releases
7978
- [x] Google your query. Simply type your query, and then put a `?` at the end,
8079
and press enter
8180
- [x] Calculator (27/12/2025)
81+
- [x] Clipboard History (29/12/2025) This works by typing `cbhist` to enter the
82+
cliboard history page, which allows u to access your clipboard history,
83+
and then use `main` to switch back, or just open an close the app again
8284

8385
### Not Possible by me:
8486

src/app.rs

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use crate::calculator::Expression;
2+
use crate::clipboard::ClipBoardContentType;
23
use crate::commands::Function;
34
use crate::config::Config;
45
use crate::macos::{focus_this_app, transform_process_to_ui_element};
56
use crate::{macos, utils::get_installed_apps};
67

8+
use arboard::Clipboard;
79
use global_hotkey::{GlobalHotKeyEvent, HotKeyState};
810
use iced::futures::SinkExt;
11+
use iced::widget::text::LineHeight;
912
use iced::{
1013
Alignment, Element, Fill, Subscription, Task, Theme,
1114
alignment::Vertical,
@@ -14,7 +17,7 @@ use iced::{
1417
stream,
1518
widget::{
1619
Button, Column, Row, Text, container, image::Viewer, operation, scrollable, space,
17-
text::LineHeight, text_input,
20+
text_input,
1821
},
1922
window::{self, Id, Settings},
2023
};
@@ -61,6 +64,7 @@ impl App {
6164
},
6265
]
6366
}
67+
6468
pub fn render(&self, theme: &crate::config::Theme) -> impl Into<iced::Element<'_, Message>> {
6569
let mut tile = Row::new().width(Fill).height(55);
6670

@@ -115,6 +119,12 @@ impl App {
115119
}
116120
}
117121

122+
#[derive(Debug, Clone, PartialEq)]
123+
pub enum Page {
124+
Main,
125+
ClipboardHistory,
126+
}
127+
118128
#[derive(Debug, Clone)]
119129
pub enum Message {
120130
OpenWindow,
@@ -126,6 +136,7 @@ pub enum Message {
126136
WindowFocusChanged(Id, bool),
127137
ClearSearchQuery,
128138
ReloadConfig,
139+
ClipboardHistory(ClipBoardContentType),
129140
_Nothing,
130141
}
131142

@@ -158,6 +169,8 @@ pub struct Tile {
158169
frontmost: Option<Retained<NSRunningApplication>>,
159170
config: Config,
160171
open_hotkey_id: u32,
172+
clipboard_content: Vec<ClipBoardContentType>,
173+
page: Page,
161174
}
162175

163176
impl Tile {
@@ -207,6 +220,8 @@ impl Tile {
207220
config: config.clone(),
208221
theme: config.theme.to_owned().into(),
209222
open_hotkey_id: keybind_id,
223+
clipboard_content: vec![],
224+
page: Page::Main,
210225
},
211226
Task::batch([open.map(|_| Message::OpenWindow)]),
212227
)
@@ -262,6 +277,10 @@ impl Tile {
262277
id,
263278
iced::Size::new(WINDOW_WIDTH, 55. + DEFAULT_WINDOW_HEIGHT),
264279
);
280+
} else if self.query_lc == "cbhist" {
281+
self.page = Page::ClipboardHistory
282+
} else if self.query_lc == "main" {
283+
self.page = Page::Main
265284
}
266285

267286
self.handle_search_query_changed();
@@ -281,15 +300,25 @@ impl Tile {
281300

282301
let max_elem = min(5, new_length);
283302

284-
if prev_size != new_length {
303+
if prev_size != new_length && self.page == Page::Main {
285304
thread::sleep(Duration::from_millis(30));
305+
286306
window::resize(
287307
id,
288308
iced::Size {
289309
width: WINDOW_WIDTH,
290310
height: ((max_elem * 55) + DEFAULT_WINDOW_HEIGHT as usize) as f32,
291311
},
292312
)
313+
} else if self.page == Page::ClipboardHistory {
314+
let element_count = min(self.clipboard_content.len(), 5);
315+
window::resize(
316+
id,
317+
iced::Size {
318+
width: WINDOW_WIDTH,
319+
height: ((element_count * 55) + DEFAULT_WINDOW_HEIGHT as usize) as f32,
320+
},
321+
)
293322
} else {
294323
Task::none()
295324
}
@@ -357,6 +386,7 @@ impl Tile {
357386
self.restore_frontmost();
358387
self.visible = false;
359388
self.focused = false;
389+
self.page = Page::Main;
360390
Task::batch([window::close(a), Task::done(Message::ClearSearchResults)])
361391
}
362392
Message::ClearSearchResults => {
@@ -373,6 +403,11 @@ impl Tile {
373403
}
374404
}
375405

406+
Message::ClipboardHistory(clip_content) => {
407+
self.clipboard_content.push(clip_content);
408+
Task::none()
409+
}
410+
376411
Message::_Nothing => Task::none(),
377412
}
378413
}
@@ -391,18 +426,31 @@ impl Tile {
391426
})
392427
.id("query")
393428
.width(Fill)
394-
.padding(20)
395-
.line_height(LineHeight::Relative(1.5));
396-
397-
let mut search_results = Column::new();
398-
for result in &self.results {
399-
search_results = search_results.push(result.render(&self.config.theme));
429+
.line_height(LineHeight::Relative(1.5))
430+
.padding(20);
431+
432+
match self.page {
433+
Page::Main => {
434+
let mut search_results = Column::new();
435+
for result in &self.results {
436+
search_results = search_results.push(result.render(&self.config.theme));
437+
}
438+
Column::new()
439+
.push(title_input)
440+
.push(scrollable(search_results))
441+
.into()
442+
}
443+
Page::ClipboardHistory => {
444+
let mut clipboard_history = Column::new();
445+
for result in &self.clipboard_content {
446+
clipboard_history = clipboard_history.push(result.render_clipboard_item());
447+
}
448+
Column::new()
449+
.push(title_input)
450+
.push(scrollable(clipboard_history))
451+
.into()
452+
}
400453
}
401-
402-
Column::new()
403-
.push(title_input)
404-
.push(scrollable(search_results))
405-
.into()
406454
} else {
407455
space().into()
408456
}
@@ -416,6 +464,7 @@ impl Tile {
416464
Subscription::batch([
417465
Subscription::run(handle_hotkeys),
418466
Subscription::run(handle_hot_reloading),
467+
Subscription::run(handle_clipboard_history),
419468
window::close_events().map(Message::HideWindow),
420469
keyboard::listen().filter_map(|event| {
421470
if let keyboard::Event::KeyPressed { key, .. } = event {
@@ -527,3 +576,31 @@ fn handle_hotkeys() -> impl futures::Stream<Item = Message> {
527576
}
528577
})
529578
}
579+
580+
fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
581+
stream::channel(100, async |mut output| {
582+
let mut clipboard = Clipboard::new().unwrap();
583+
let mut prev_byte_rep: Option<ClipBoardContentType> = None;
584+
585+
loop {
586+
let byte_rep = if let Ok(a) = clipboard.get_image() {
587+
Some(ClipBoardContentType::Image(a))
588+
} else if let Ok(a) = clipboard.get_text() {
589+
Some(ClipBoardContentType::Text(a))
590+
} else {
591+
None
592+
};
593+
594+
if byte_rep != prev_byte_rep
595+
&& let Some(content) = &byte_rep
596+
{
597+
output
598+
.send(Message::ClipboardHistory(content.to_owned()))
599+
.await
600+
.ok();
601+
prev_byte_rep = byte_rep;
602+
}
603+
tokio::time::sleep(Duration::from_millis(10)).await;
604+
}
605+
})
606+
}

src/clipboard.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use arboard::ImageData;
2+
use iced::{
3+
Length::Fill,
4+
Theme,
5+
alignment::Vertical,
6+
widget::{Button, Row, Text, container},
7+
};
8+
9+
use crate::{app::Message, commands::Function};
10+
11+
#[derive(Debug, Clone)]
12+
pub enum ClipBoardContentType {
13+
Text(String),
14+
Image(ImageData<'static>),
15+
}
16+
17+
impl ClipBoardContentType {
18+
pub fn render_clipboard_item(&self) -> impl Into<iced::Element<'_, Message>> {
19+
let mut tile = Row::new().width(Fill).height(55);
20+
21+
let text = match self {
22+
ClipBoardContentType::Text(text) => text,
23+
ClipBoardContentType::Image(_) => "<img>",
24+
};
25+
26+
tile = tile.push(
27+
Button::new(
28+
Text::new(text.to_owned())
29+
.height(Fill)
30+
.width(Fill)
31+
.align_y(Vertical::Center),
32+
)
33+
.on_press(Message::RunFunction(Function::CopyToClipboard(
34+
self.to_owned(),
35+
)))
36+
.style(|_, _| iced::widget::button::Style {
37+
background: Some(iced::Background::Color(
38+
Theme::KanagawaDragon.palette().background,
39+
)),
40+
text_color: Theme::KanagawaDragon.palette().text,
41+
..Default::default()
42+
})
43+
.width(Fill)
44+
.height(55),
45+
);
46+
47+
container(tile)
48+
.style(|_| iced::widget::container::Style {
49+
text_color: Some(Theme::KanagawaDragon.palette().text),
50+
background: Some(iced::Background::Color(
51+
Theme::KanagawaDragon.palette().background,
52+
)),
53+
..Default::default()
54+
})
55+
.width(Fill)
56+
.height(Fill)
57+
}
58+
}
59+
60+
impl PartialEq for ClipBoardContentType {
61+
fn eq(&self, other: &Self) -> bool {
62+
if let Self::Text(a) = self
63+
&& let Self::Text(b) = other
64+
{
65+
return a == b;
66+
} else if let Self::Image(image_data) = self
67+
&& let Self::Image(other_image_data) = other
68+
{
69+
return image_data.bytes == other_image_data.bytes;
70+
}
71+
false
72+
}
73+
}

src/commands.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ use arboard::Clipboard;
44
use objc2_app_kit::NSWorkspace;
55
use objc2_foundation::NSURL;
66

7-
use crate::{calculator::Expression, config::Config};
7+
use crate::{calculator::Expression, clipboard::ClipBoardContentType, config::Config};
88

99
#[derive(Debug, Clone)]
1010
pub enum Function {
1111
OpenApp(String),
1212
RunShellCommand(String, String),
1313
RandomVar(i32),
14+
CopyToClipboard(ClipBoardContentType),
1415
GoogleSearch(String),
1516
Calculate(Expression),
1617
OpenPrefPane,
@@ -67,6 +68,15 @@ impl Function {
6768
.unwrap_or(());
6869
}
6970

71+
Function::CopyToClipboard(clipboard_content) => match clipboard_content {
72+
ClipBoardContentType::Text(text) => {
73+
Clipboard::new().unwrap().set_text(text).ok();
74+
}
75+
ClipBoardContentType::Image(img) => {
76+
Clipboard::new().unwrap().set_image(img.to_owned_img()).ok();
77+
}
78+
},
79+
7080
Function::OpenPrefPane => {
7181
thread::spawn(move || {
7282
NSWorkspace::new().openURL(&NSURL::fileURLWithPath(

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod app;
22
mod calculator;
3+
mod clipboard;
34
mod commands;
45
mod config;
56
mod macos;

0 commit comments

Comments
 (0)