diff --git a/README.md b/README.md index e5d8922..79ce903 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ bit wonky, and will be fixed in the upcoming releases - [ ] Select the options using arrow keys - [ ] Popup note-taking -- [ ] Clipboard History - [ ] Plugin Support (Partially implemented on 15/12/2025) - [ ] Blur / transparent background (Partially implemented on 13/12/2025) - [ ] Hyperkey - Map CMD + OPT + CTRL + SHIFT to a physical key @@ -79,6 +78,9 @@ bit wonky, and will be fixed in the upcoming releases - [x] Google your query. Simply type your query, and then put a `?` at the end, and press enter - [x] Calculator (27/12/2025) +- [x] Clipboard History (29/12/2025) This works by typing `cbhist` to enter the + cliboard history page, which allows u to access your clipboard history, + and then use `main` to switch back, or just open an close the app again ### Not Possible by me: diff --git a/src/app.rs b/src/app.rs index b76e341..7d7267b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,14 @@ use crate::calculator::Expression; +use crate::clipboard::ClipBoardContentType; use crate::commands::Function; use crate::config::Config; use crate::macos::{focus_this_app, transform_process_to_ui_element}; use crate::{macos, utils::get_installed_apps}; +use arboard::Clipboard; use global_hotkey::{GlobalHotKeyEvent, HotKeyState}; use iced::futures::SinkExt; +use iced::widget::text::LineHeight; use iced::{ Alignment, Element, Fill, Subscription, Task, Theme, alignment::Vertical, @@ -14,7 +17,7 @@ use iced::{ stream, widget::{ Button, Column, Row, Text, container, image::Viewer, operation, scrollable, space, - text::LineHeight, text_input, + text_input, }, window::{self, Id, Settings}, }; @@ -61,6 +64,7 @@ impl App { }, ] } + pub fn render(&self, theme: &crate::config::Theme) -> impl Into> { let mut tile = Row::new().width(Fill).height(55); @@ -115,6 +119,12 @@ impl App { } } +#[derive(Debug, Clone, PartialEq)] +pub enum Page { + Main, + ClipboardHistory, +} + #[derive(Debug, Clone)] pub enum Message { OpenWindow, @@ -126,6 +136,7 @@ pub enum Message { WindowFocusChanged(Id, bool), ClearSearchQuery, ReloadConfig, + ClipboardHistory(ClipBoardContentType), _Nothing, } @@ -158,6 +169,8 @@ pub struct Tile { frontmost: Option>, config: Config, open_hotkey_id: u32, + clipboard_content: Vec, + page: Page, } impl Tile { @@ -207,6 +220,8 @@ impl Tile { config: config.clone(), theme: config.theme.to_owned().into(), open_hotkey_id: keybind_id, + clipboard_content: vec![], + page: Page::Main, }, Task::batch([open.map(|_| Message::OpenWindow)]), ) @@ -262,6 +277,10 @@ impl Tile { id, iced::Size::new(WINDOW_WIDTH, 55. + DEFAULT_WINDOW_HEIGHT), ); + } else if self.query_lc == "cbhist" { + self.page = Page::ClipboardHistory + } else if self.query_lc == "main" { + self.page = Page::Main } self.handle_search_query_changed(); @@ -281,8 +300,9 @@ impl Tile { let max_elem = min(5, new_length); - if prev_size != new_length { + if prev_size != new_length && self.page == Page::Main { thread::sleep(Duration::from_millis(30)); + window::resize( id, iced::Size { @@ -290,6 +310,15 @@ impl Tile { height: ((max_elem * 55) + DEFAULT_WINDOW_HEIGHT as usize) as f32, }, ) + } else if self.page == Page::ClipboardHistory { + let element_count = min(self.clipboard_content.len(), 5); + window::resize( + id, + iced::Size { + width: WINDOW_WIDTH, + height: ((element_count * 55) + DEFAULT_WINDOW_HEIGHT as usize) as f32, + }, + ) } else { Task::none() } @@ -357,6 +386,7 @@ impl Tile { self.restore_frontmost(); self.visible = false; self.focused = false; + self.page = Page::Main; Task::batch([window::close(a), Task::done(Message::ClearSearchResults)]) } Message::ClearSearchResults => { @@ -373,6 +403,11 @@ impl Tile { } } + Message::ClipboardHistory(clip_content) => { + self.clipboard_content.push(clip_content); + Task::none() + } + Message::_Nothing => Task::none(), } } @@ -391,18 +426,31 @@ impl Tile { }) .id("query") .width(Fill) - .padding(20) - .line_height(LineHeight::Relative(1.5)); - - let mut search_results = Column::new(); - for result in &self.results { - search_results = search_results.push(result.render(&self.config.theme)); + .line_height(LineHeight::Relative(1.5)) + .padding(20); + + match self.page { + Page::Main => { + let mut search_results = Column::new(); + for result in &self.results { + search_results = search_results.push(result.render(&self.config.theme)); + } + Column::new() + .push(title_input) + .push(scrollable(search_results)) + .into() + } + Page::ClipboardHistory => { + let mut clipboard_history = Column::new(); + for result in &self.clipboard_content { + clipboard_history = clipboard_history.push(result.render_clipboard_item()); + } + Column::new() + .push(title_input) + .push(scrollable(clipboard_history)) + .into() + } } - - Column::new() - .push(title_input) - .push(scrollable(search_results)) - .into() } else { space().into() } @@ -416,6 +464,7 @@ impl Tile { Subscription::batch([ Subscription::run(handle_hotkeys), Subscription::run(handle_hot_reloading), + Subscription::run(handle_clipboard_history), window::close_events().map(Message::HideWindow), keyboard::listen().filter_map(|event| { if let keyboard::Event::KeyPressed { key, .. } = event { @@ -527,3 +576,31 @@ fn handle_hotkeys() -> impl futures::Stream { } }) } + +fn handle_clipboard_history() -> impl futures::Stream { + stream::channel(100, async |mut output| { + let mut clipboard = Clipboard::new().unwrap(); + let mut prev_byte_rep: Option = None; + + loop { + let byte_rep = if let Ok(a) = clipboard.get_image() { + Some(ClipBoardContentType::Image(a)) + } else if let Ok(a) = clipboard.get_text() { + Some(ClipBoardContentType::Text(a)) + } else { + None + }; + + if byte_rep != prev_byte_rep + && let Some(content) = &byte_rep + { + output + .send(Message::ClipboardHistory(content.to_owned())) + .await + .ok(); + prev_byte_rep = byte_rep; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) +} diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..fb79447 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,73 @@ +use arboard::ImageData; +use iced::{ + Length::Fill, + Theme, + alignment::Vertical, + widget::{Button, Row, Text, container}, +}; + +use crate::{app::Message, commands::Function}; + +#[derive(Debug, Clone)] +pub enum ClipBoardContentType { + Text(String), + Image(ImageData<'static>), +} + +impl ClipBoardContentType { + pub fn render_clipboard_item(&self) -> impl Into> { + let mut tile = Row::new().width(Fill).height(55); + + let text = match self { + ClipBoardContentType::Text(text) => text, + ClipBoardContentType::Image(_) => "", + }; + + tile = tile.push( + Button::new( + Text::new(text.to_owned()) + .height(Fill) + .width(Fill) + .align_y(Vertical::Center), + ) + .on_press(Message::RunFunction(Function::CopyToClipboard( + self.to_owned(), + ))) + .style(|_, _| iced::widget::button::Style { + background: Some(iced::Background::Color( + Theme::KanagawaDragon.palette().background, + )), + text_color: Theme::KanagawaDragon.palette().text, + ..Default::default() + }) + .width(Fill) + .height(55), + ); + + container(tile) + .style(|_| iced::widget::container::Style { + text_color: Some(Theme::KanagawaDragon.palette().text), + background: Some(iced::Background::Color( + Theme::KanagawaDragon.palette().background, + )), + ..Default::default() + }) + .width(Fill) + .height(Fill) + } +} + +impl PartialEq for ClipBoardContentType { + fn eq(&self, other: &Self) -> bool { + if let Self::Text(a) = self + && let Self::Text(b) = other + { + return a == b; + } else if let Self::Image(image_data) = self + && let Self::Image(other_image_data) = other + { + return image_data.bytes == other_image_data.bytes; + } + false + } +} diff --git a/src/commands.rs b/src/commands.rs index 1a050ad..1a97c80 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,13 +4,14 @@ use arboard::Clipboard; use objc2_app_kit::NSWorkspace; use objc2_foundation::NSURL; -use crate::{calculator::Expression, config::Config}; +use crate::{calculator::Expression, clipboard::ClipBoardContentType, config::Config}; #[derive(Debug, Clone)] pub enum Function { OpenApp(String), RunShellCommand(String, String), RandomVar(i32), + CopyToClipboard(ClipBoardContentType), GoogleSearch(String), Calculate(Expression), OpenPrefPane, @@ -67,6 +68,15 @@ impl Function { .unwrap_or(()); } + Function::CopyToClipboard(clipboard_content) => match clipboard_content { + ClipBoardContentType::Text(text) => { + Clipboard::new().unwrap().set_text(text).ok(); + } + ClipBoardContentType::Image(img) => { + Clipboard::new().unwrap().set_image(img.to_owned_img()).ok(); + } + }, + Function::OpenPrefPane => { thread::spawn(move || { NSWorkspace::new().openURL(&NSURL::fileURLWithPath( diff --git a/src/main.rs b/src/main.rs index 113fa11..6b66de4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod app; mod calculator; +mod clipboard; mod commands; mod config; mod macos;