Skip to content
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
1,440 changes: 1,423 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ once_cell = "1.21.3"
rand = "0.9.2"
rayon = "1.11.0"
serde = { version = "1.0.228", features = ["derive"] }
skim = {version = "1.8.1", default-features = false}
tokio = { version = "1.48.0", features = ["full"] }
toml = "0.9.8"
tray-icon = "0.21.3"
Expand Down
51 changes: 20 additions & 31 deletions src/app/apps.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! This modules handles the logic for each "app" that rustcast can load
//!
//! An "app" is effectively, one of the results that rustcast returns when you search for something
use std::path::Path;
use std::{borrow::Cow, path::Path};

use iced::{
Alignment,
Expand Down Expand Up @@ -34,18 +34,14 @@ pub enum AppCommand {
#[derive(Debug, Clone)]
pub struct App {
pub open_command: AppCommand,
pub desc: String,
pub desc: Cow<'static, str>,
pub icons: Option<iced::widget::image::Handle>,
pub name: String,
pub name_lc: String,
pub name: Cow<'static, str>,
}

impl PartialEq for App {
fn eq(&self, other: &Self) -> bool {
self.name_lc == other.name_lc
&& self.icons == other.icons
&& self.desc == other.desc
&& self.name == other.name
self.icons == other.icons && self.desc == other.desc && self.name == other.name
}
}

Expand All @@ -54,14 +50,13 @@ impl App {
pub fn emoji_apps() -> Vec<App> {
emojis::iter()
.filter(|x| x.unicode_version() < emojis::UnicodeVersion::new(17, 13))
.map(|x| App {
.map(|emoji| App {
icons: None,
name: x.to_string(),
name_lc: x.name().to_string(),
name: Cow::Borrowed(emoji.as_str()),
open_command: AppCommand::Function(Function::CopyToClipboard(
ClipBoardContentType::Text(x.to_string()),
ClipBoardContentType::Text(Cow::Borrowed(emoji.as_str())),
)),
desc: x.name().to_string(),
desc: Cow::Borrowed(emoji.name()),
})
.collect()
}
Expand All @@ -72,57 +67,51 @@ impl App {
vec![
App {
open_command: AppCommand::Function(Function::Quit),
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: "Quit RustCast".to_string(),
name_lc: "quit".to_string(),
name: Cow::Borrowed("Quit RustCast"),
},
App {
open_command: AppCommand::Function(Function::OpenPrefPane),
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: "Open RustCast Preferences".to_string(),
name_lc: "settings".to_string(),
name: Cow::Borrowed("Open RustCast Preferences"),
},
App {
open_command: AppCommand::Message(Message::SwitchToPage(Page::EmojiSearch)),
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: "Search for an Emoji".to_string(),
name_lc: "emoji".to_string(),
name: Cow::Borrowed("Search for an Emoji"),
},
App {
open_command: AppCommand::Message(Message::SwitchToPage(Page::ClipboardHistory)),
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: "Clipboard History".to_string(),
name_lc: "clipboard".to_string(),
name: Cow::Borrowed("Clipboard History"),
},
App {
open_command: AppCommand::Message(Message::ReloadConfig),
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: "Reload RustCast".to_string(),
name_lc: "refresh".to_string(),
name: Cow::Borrowed("Reload RustCast"),
},
App {
open_command: AppCommand::Display,
desc: RUSTCAST_DESC_NAME.to_string(),
desc: Cow::Borrowed(RUSTCAST_DESC_NAME),
icons: handle_from_icns(Path::new(
"/Applications/Rustcast.app/Contents/Resources/icon.icns",
)),
name: format!("Current RustCast Version: {app_version}"),
name_lc: "version".to_string(),
name: Cow::Owned(format!("Current RustCast Version: {app_version}")),
},
]
}
Expand Down
6 changes: 4 additions & 2 deletions src/app/pages/clipboard.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;

use iced::widget::{
Scrollable, scrollable,
scrollable::{Direction, Scrollbar},
Expand Down Expand Up @@ -30,8 +32,8 @@ pub fn clipboard_view(
Text::new(
clipboard_content
.get(focussed_id as usize)
.map(|x| x.to_app().name_lc)
.unwrap_or("".to_string()),
.map(|entry| entry.to_app().name)
.unwrap_or(Cow::Borrowed("")),
)
.height(385)
.width(Length::Fill)
Expand Down
82 changes: 42 additions & 40 deletions src/app/tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ use iced::{event, window};
use objc2::rc::Retained;
use objc2_app_kit::NSRunningApplication;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use skim::{fuzzy_matcher::FuzzyMatcher, prelude::SkimMatcherV2};
use tray_icon::TrayIcon;

use std::fs;
use std::ops::Bound;
use std::{borrow::Cow, fs};

use std::path::Path;
use std::time::Duration;
use std::{collections::BTreeMap, path::Path};

/// This is a wrapper around the sender to disable dropping
#[derive(Clone, Debug)]
Expand All @@ -42,52 +43,52 @@ impl Drop for ExtSender {
}

/// All the indexed apps that rustcast can search for
#[derive(Clone, Debug)]
#[derive(Debug)]
struct AppIndex {
by_name: BTreeMap<String, App>,
apps: Vec<App>,
matcher: SkimMatcherV2,
}

impl AppIndex {
/// Search for an element in the index that starts with the provided prefix
fn search_prefix<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = &'a App> + 'a {
self.by_name
.range::<str, _>((Bound::Included(prefix), Bound::Unbounded))
.take_while(move |(k, _)| k.starts_with(prefix))
.map(|(_, v)| v)
fn search(&self, pattern: &str) -> Vec<(i64, &App)> {
let mut selected: Vec<_> = self
.apps
.iter()
.filter_map(|app| self.matcher.fuzzy_match(&app.name, pattern).zip(Some(app)))
.collect();
selected.sort_by(|(score1, _), (score2, _)| score2.cmp(score1));

selected
}

/// Factory function for creating
pub fn from_apps(options: Vec<App>) -> Self {
let mut bmap = BTreeMap::new();
for app in options {
bmap.insert(app.name_lc.clone(), app);
pub fn from_apps(apps: Vec<App>) -> Self {
Self {
apps,
matcher: SkimMatcherV2::default(),
}

AppIndex { by_name: bmap }
}
}

/// This is the base window, and its a "Tile"
/// Its fields are:
/// - Theme ([`iced::Theme`])
/// - Query (String)
/// - Query Lowercase (String, but lowercase)
/// - Previous Query Lowercase (String)
/// - Results (Vec<[`App`]>) the results of the search
/// - Options (Vec<[`App`]>) the options to search through
/// - Visible (bool) whether the window is visible or not
/// - Focused (bool) whether the window is focused or not
/// - Frontmost ([`Option<Retained<NSRunningApplication>>`]) the frontmost application before the window was opened
/// - Config ([`Config`]) the app's config
/// - Open Hotkey ID (`u32`) the id of the hotkey that opens the window
/// - Clipboard Content (`Vec<`[`ClipBoardContentType`]`>`) all of the cliboard contents
/// - Page ([`Page`]) the current page of the window (main or clipboard history)
#[derive(Clone)]
/// - Theme ([`iced::Theme`]).
/// - Query (String).
/// - Query Lowercase (String, but lowercase).
/// - Previous Query Lowercase (String).
/// - Results (Vec<[`App`]>) the results of the search.
/// - Options (Vec<[`App`]>) the options to search through.
/// - Visible (bool) whether the window is visible or not.
/// - Focused (bool) whether the window is focused or not.
/// - Frontmost ([`Option<Retained<NSRunningApplication>>`]) the frontmost application before the window was opened.
/// - Config ([`Config`]) the app's config.
/// - Open Hotkey ID (`u32`) the id of the hotkey that opens the window.
/// - Clipboard Content (`Vec<`[`ClipBoardContentType`]`>`) all of the cliboard contents.
/// - Page ([`Page`]) the current page of the window (main or clipboard history).
pub struct Tile {
pub theme: iced::Theme,
pub focus_id: u32,
pub query: String,
query_lc: String,
results: Vec<App>,
options: AppIndex,
emoji_apps: AppIndex,
Expand Down Expand Up @@ -206,20 +207,21 @@ impl Tile {
/// should be separated out to make it easier to test. This function is called by the `update`
/// function to handle the search query changed event.
pub fn handle_search_query_changed(&mut self) {
let query = self.query_lc.clone();
let options = if self.page == Page::Main {
&self.options
} else if self.page == Page::EmojiSearch {
&self.emoji_apps
} else {
&AppIndex::from_apps(vec![])
};
let results: Vec<App> = options
.search_prefix(&query)
.map(|x| x.to_owned())
.collect();

self.results = results;
self.results.clear();
self.results.extend(
options
.search(&self.query)
.into_iter()
.map(|(_, app)| app.clone()),
);
}

/// Gets the frontmost application to focus later.
Expand Down Expand Up @@ -316,8 +318,8 @@ fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
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 if let Ok(value) = clipboard.get_text() {
Some(ClipBoardContentType::Text(Cow::Owned(value)))
} else {
None
};
Expand Down
6 changes: 3 additions & 3 deletions src/app/tile/elm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
(
Tile {
query: String::new(),
query_lc: String::new(),
focus_id: 0,
results: vec![],
options,
Expand Down Expand Up @@ -110,8 +109,9 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> {
emoji_page(
tile.config.theme.clone(),
tile.emoji_apps
.search_prefix(&tile.query_lc)
.map(|x| x.to_owned())
.search(&tile.query)
.into_iter()
.map(|(_, app)| app.clone())
.collect(),
tile.focus_id,
)
Expand Down
Loading