Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
103 changes: 90 additions & 13 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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},
};
Expand Down Expand Up @@ -61,6 +64,7 @@ impl App {
},
]
}

pub fn render(&self, theme: &crate::config::Theme) -> impl Into<iced::Element<'_, Message>> {
let mut tile = Row::new().width(Fill).height(55);

Expand Down Expand Up @@ -115,6 +119,12 @@ impl App {
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum Page {
Main,
ClipboardHistory,
}

#[derive(Debug, Clone)]
pub enum Message {
OpenWindow,
Expand All @@ -126,6 +136,7 @@ pub enum Message {
WindowFocusChanged(Id, bool),
ClearSearchQuery,
ReloadConfig,
ClipboardHistory(ClipBoardContentType),
_Nothing,
}

Expand Down Expand Up @@ -158,6 +169,8 @@ pub struct Tile {
frontmost: Option<Retained<NSRunningApplication>>,
config: Config,
open_hotkey_id: u32,
clipboard_content: Vec<ClipBoardContentType>,
page: Page,
}

impl Tile {
Expand Down Expand Up @@ -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)]),
)
Expand Down Expand Up @@ -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();
Expand All @@ -281,15 +300,25 @@ 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 {
width: WINDOW_WIDTH,
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()
}
Expand Down Expand Up @@ -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 => {
Expand All @@ -373,6 +403,11 @@ impl Tile {
}
}

Message::ClipboardHistory(clip_content) => {
self.clipboard_content.push(clip_content);
Task::none()
}

Message::_Nothing => Task::none(),
}
}
Expand All @@ -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()
}
Expand All @@ -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 {
Expand Down Expand Up @@ -527,3 +576,31 @@ fn handle_hotkeys() -> impl futures::Stream<Item = Message> {
}
})
}

fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
stream::channel(100, async |mut output| {
let mut clipboard = Clipboard::new().unwrap();
let mut prev_byte_rep: Option<ClipBoardContentType> = 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;
}
})
}
73 changes: 73 additions & 0 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -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<iced::Element<'_, Message>> {
let mut tile = Row::new().width(Fill).height(55);

let text = match self {
ClipBoardContentType::Text(text) => text,
ClipBoardContentType::Image(_) => "<img>",
};

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
}
}
12 changes: 11 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod app;
mod calculator;
mod clipboard;
mod commands;
mod config;
mod macos;
Expand Down