From ff8c6dcdf239e0b5b69eb138bb5650bb855192d9 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Thu, 26 Mar 2026 15:53:35 +0800
Subject: [PATCH 01/51] fix links
---
CONTRIBUTING.md | 2 +-
Cargo.toml | 2 +-
README.md | 36 ++++++++++++++++++------------------
docs/index.html | 26 +++++++++++++-------------
src/app/menubar.rs | 8 ++++----
src/app/pages/settings.rs | 2 +-
src/app/tile.rs | 2 +-
7 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d10dddcd..c321d5e6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ There are 2 areas you can work on:
1. Help people in solving their github issues
For bug fixes, and helping people to solve their github issues: see
-[https://github.com/unsecretised/rustcast/issues] For features, see
+[https://github.com/RustCastLabs/rustcast/issues] For features, see
[The Planned Features in the README](README.md) or
[The existing feature list](FEATURES.md)
diff --git a/Cargo.toml b/Cargo.toml
index 65fbdbdb..89afae40 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2024"
license = "MIT"
description = "RustCast - Productivity app for macOS"
homepage = "https://rustcast.app"
-repository = "https://github.com/unsecretised/rustcast"
+repository = "https://github.com/RustCastLabs/rustcast"
[dependencies]
anyhow = "1.0.100"
diff --git a/README.md b/README.md
index b9757316..5c5ba3d9 100644
--- a/README.md
+++ b/README.md
@@ -10,35 +10,35 @@
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-> [Those who sponsor me also get a personal easter egg inside RustCast](https://github.com/sponsors/unsecretised)
+> [Those who sponsor me also get a personal easter egg inside RustCast](https://github.com/sponsors/RustCastLabs)
-**Config docs:** https://github.com/unsecretised/rustcast/wiki
+**Config docs:** https://github.com/RustCastLabs/rustcast/wiki
**Community:** https://discord.gg/bDfNYPbnC5
**Plugins**:
-[RustCast Library for shell scripts](https://github.com/unsecretised/rustcast-library)
+[RustCast Library for shell scripts](https://github.com/RustCastLabs/rustcast-library)
> For support use github discussions / issues / the discord
>
-> You can also contact unsecretised / secretised at
+> You can also contact RustCastLabs / secretised at
> [admin@rustcast.app](mailto:admin+gh@rustcast.app)

@@ -48,18 +48,18 @@
### Via Homebrew:
```
-brew install --cask unsecretised/tap/rustcast
+brew install --cask RustCastLabs/tap/rustcast
```
### Via github releases
1. Download the dmg from this link
- [https://github.com/unsecretised/rustcast/releases/latest/download/rustcast.dmg](https://github.com/unsecretised/rustcast/releases/latest/download/rustcast.dmg)
+ [https://github.com/RustCastLabs/rustcast/releases/latest/download/rustcast.dmg](https://github.com/RustCastLabs/rustcast/releases/latest/download/rustcast.dmg)
## Config:
Full config docs can be found
-[here](https://github.com/unsecretised/rustcast/wiki)
+[here](https://github.com/RustCastLabs/rustcast/wiki)
The config file should be located at: `~/.config/rustcast/config.toml` RustCast
creates the default configuration for you, but it does use its
@@ -135,8 +135,8 @@ And ofc, all the people who starred my repo!!
And the updated list of contributors to the macos version:
-
-
+
+
### Easter egg list:
@@ -148,7 +148,7 @@ And the updated list of contributors to the macos version:
## If you like rustcast, consider starring it on github :)
-[](https://www.star-history.com/#unsecretised/rustcast&type=date&legend=top-left)
+[](https://www.star-history.com/#RustCastLabs/rustcast&type=date&legend=top-left)
## Motivations:
diff --git a/docs/index.html b/docs/index.html
index 00ce1c22..d81c8f71 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -22,19 +22,19 @@
Features
Install
- GitHub
Download
@@ -61,13 +61,13 @@ The fastest launcher
you'll ever use
- brew tap unsecretised/tap
+ brew tap RustCastLabs/tap
brew install --cask rustcast
@@ -370,7 +370,7 @@ Up and running
in 30 seconds
@@ -381,7 +381,7 @@ Up and running
in 30 seconds
git clone
- https://github.com/unsecretised/rustcast.git
+ https://github.com/RustCastLabs/rustcast.git
cd rustcast
cargo install --path .
@@ -481,7 +481,7 @@ Built by the community
- brew tap RustCastLabs/tap
+ brew tap unsecretised/tap
brew install --cask rustcast
From 08086a0bb987bf5b18fe2ee0ae807852b757955f Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sat, 28 Mar 2026 10:21:50 +0800
Subject: [PATCH 26/51] add ability to opt out of clipboard history
---
src/app.rs | 1 +
src/app/pages/settings.rs | 18 ++++++++++++++++++
src/app/tile/update.rs | 24 ++++++++++++++++++++----
src/config.rs | 2 ++
4 files changed, 41 insertions(+), 4 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index dc900711..160707b1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -124,6 +124,7 @@ pub enum SetConfigFields {
ClipboardHotkey(String),
PlaceHolder(String),
SearchUrl(String),
+ ClipboardHistory(bool),
HapticFeedback(bool),
ShowMenubarIcon(bool),
SetPage(MainPage),
diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs
index 71742ba4..5a040713 100644
--- a/src/app/pages/settings.rs
+++ b/src/app/pages/settings.rs
@@ -83,6 +83,23 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
notice_item(theme.clone(), "Which search engine to use (%s = query)"),
]);
+ let theme_clone = theme.clone();
+ let clipboard_history = Row::from_iter([
+ settings_hint_text(theme.clone(), "Enable Clipboard history"),
+ checkbox(config.clone().cbhist)
+ .style(move |_, _| settings_checkbox_style(&theme_clone))
+ .on_toggle(|input| Message::SetConfig(SetConfigFields::ClipboardHistory(input)))
+ .into(),
+ notice_item(
+ theme.clone(),
+ "If you want your clipboard history to be stored",
+ ),
+ ])
+ .align_y(Alignment::Center)
+ .spacing(SETTINGS_ITEM_COL_SPACING * 2)
+ .padding(SETTINGS_ITEM_PADDING)
+ .height(SETTINGS_ITEM_HEIGHT);
+
let theme_clone = theme.clone();
let current_delay = config.debounce_delay;
let debounce = settings_item_column([
@@ -396,6 +413,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
debounce.into(),
haptic.into(),
tray_icon.into(),
+ clipboard_history.into(),
auto_suggest.into(),
show_scrollbar.into(),
clear_on_hide.into(),
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index 8cac396b..c9d8b895 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -345,9 +345,20 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
}
Message::SwitchToPage(page) => {
- tile.page = page;
- let task = match tile.page {
- Page::ClipboardHistory | Page::Settings => window::latest().map(|x| {
+ let task = match &page {
+ Page::ClipboardHistory => {
+ if !tile.config.cbhist {
+ return Task::none();
+ }
+ window::latest().map(|x| {
+ let id = x.unwrap();
+ Message::ResizeWindow(
+ id,
+ ((7 * 55) + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32,
+ )
+ })
+ }
+ Page::Settings => window::latest().map(|x| {
let id = x.unwrap();
Message::ResizeWindow(
id,
@@ -357,6 +368,8 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
_ => Task::none(),
};
+ tile.page = page;
+
let refresh_empty_main_query = if tile.page == Page::Main {
window::latest()
.map(|x| x.unwrap())
@@ -477,6 +490,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
}
Message::EditClipboardHistory(action) => {
+ if !tile.config.cbhist {
+ return Task::none();
+ }
match action {
Editable::Create(content) => {
if !tile.clipboard_content.contains(&content) {
@@ -606,6 +622,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
match config {
SetConfigFields::ToggleHotkey(hk) => final_config.toggle_hotkey = hk,
SetConfigFields::ClipboardHotkey(hk) => final_config.clipboard_hotkey = hk,
+ SetConfigFields::ClipboardHistory(cbhist) => final_config.cbhist = cbhist,
SetConfigFields::Modes(Editable::Create((key, value))) => {
final_config.modes.insert(key, value);
}
@@ -935,7 +952,6 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task {
}
"cbhist" => {
task = task.chain(Task::done(Message::SwitchToPage(Page::ClipboardHistory)));
- tile.page = Page::ClipboardHistory;
}
"main" => {
if tile.page != Page::Main {
diff --git a/src/config.rs b/src/config.rs
index 667fa76e..cddb8395 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -25,6 +25,7 @@ pub struct Config {
pub placeholder: String,
pub search_url: String,
pub haptic_feedback: bool,
+ pub cbhist: bool,
pub show_trayicon: bool,
pub shells: Vec,
pub modes: HashMap,
@@ -44,6 +45,7 @@ impl Default for Config {
theme: Theme::default(),
placeholder: String::from("Time to be productive!"),
search_url: "https://duckduckgo.com/search?q=%s".to_string(),
+ cbhist: true,
haptic_feedback: false,
show_trayicon: true,
main_page: MainPage::default(),
From 358149e7e7050aa4b18d2bd821c2b9bfefb27f0b Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sat, 28 Mar 2026 14:44:56 +0800
Subject: [PATCH 27/51] update styling
---
src/styles.rs | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/styles.rs b/src/styles.rs
index 869a77d2..dd0d386b 100644
--- a/src/styles.rs
+++ b/src/styles.rs
@@ -27,14 +27,14 @@ pub fn rustcast_text_input_style(theme: &ConfigTheme) -> text_input::Style {
text_input::Style {
background: Background::Color(surface),
border: Border {
- color: glass_border(theme.text_color(1.0), focused),
+ color: glass_border(theme.text_color(0.), focused),
width: 0.,
- radius: Radius::new(15.).bottom(0.),
+ radius: Radius::new(10.).bottom(0.),
},
- icon: theme.text_color(0.75),
- placeholder: theme.text_color(0.50),
- value: theme.text_color(1.0),
- selection: with_alpha(theme.text_color(1.0), 0.20),
+ icon: theme.text_color(0.),
+ placeholder: theme.text_color(0.2),
+ value: theme.text_color(0.9),
+ selection: theme.text_color(0.2),
}
}
@@ -44,7 +44,7 @@ pub fn contents_style(theme: &ConfigTheme) -> container::Style {
background: None,
text_color: None,
border: iced::Border {
- color: theme.text_color(0.7),
+ color: theme.text_color(0.9),
width: 0.4,
radius: Radius::new(14.0),
},
@@ -260,8 +260,8 @@ pub fn settings_slider_style(theme: &ConfigTheme) -> slider::Style {
/// Helper fn for making a color look like its glassy
pub fn glass_surface(base: Color, focused: bool) -> Color {
- let t = if focused { 0.3 } else { 0.06 };
- let a = if focused { 0.3 } else { 0.22 };
+ let t = if focused { 0.2 } else { 0.06 };
+ let a = if focused { 0.9 } else { 0.58 };
with_alpha(tint(base, t), a)
}
From ff1cf0cd3c59d0de2e7ad799d95f040bb52772dc Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sun, 29 Mar 2026 17:36:21 +0800
Subject: [PATCH 28/51] Switch to NSEvent handling for hotkeys
---
src/app.rs | 3 +-
src/app/menubar.rs | 34 ++---
src/app/pages/settings.rs | 4 +-
src/app/tile.rs | 32 +---
src/app/tile/elm.rs | 23 +--
src/app/tile/update.rs | 49 ++++++-
src/main.rs | 39 ++---
src/platform/macos/launching.rs | 250 ++++++++++++++++++++++++++++++++
src/platform/macos/mod.rs | 1 +
9 files changed, 339 insertions(+), 96 deletions(-)
create mode 100644 src/platform/macos/launching.rs
diff --git a/src/app.rs b/src/app.rs
index 2be7378c..68b19e41 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,6 +5,7 @@ use crate::app::apps::{App, AppCommand, ICNS_ICON};
use crate::commands::Function;
use crate::config::{Config, MainPage, Shelly};
use crate::debounce::DebouncePolicy;
+use crate::platform::macos::launching::Shortcut;
use crate::utils::icns_data_to_handle;
use crate::{app::tile::ExtSender, clipboard::ClipBoardContentType};
use iced::time::Duration;
@@ -90,7 +91,7 @@ pub enum Message {
OpenResult(u32),
OpenToSettings,
SearchQueryChanged(String, Id),
- KeyPressed(u32),
+ KeyPressed(Shortcut),
FocusTextInput(Move),
HideWindow(Id),
RunFunction(Function),
diff --git a/src/app/menubar.rs b/src/app/menubar.rs
index b9a566f6..817a6e1d 100644
--- a/src/app/menubar.rs
+++ b/src/app/menubar.rs
@@ -2,7 +2,7 @@
use std::{collections::HashMap, io::Cursor};
-use global_hotkey::hotkey::{Code, HotKey, Modifiers};
+use global_hotkey::hotkey::{Code, Modifiers};
use image::{DynamicImage, ImageReader};
use log::info;
use tray_icon::{
@@ -16,6 +16,7 @@ use tray_icon::{
use crate::{
app::{Message, tile::ExtSender},
config::Config,
+ platform::macos::launching::Shortcut,
utils::open_url,
};
@@ -39,14 +40,15 @@ pub fn menu_icon(config: Config, sender: ExtSender) -> TrayIcon {
}
pub fn menu_builder(config: Config, sender: ExtSender, update_item: bool) -> Menu {
- let hotkey = config.toggle_hotkey.parse::().ok();
+ let shortcut =
+ Shortcut::parse(&config.toggle_hotkey).unwrap_or(Shortcut::parse("opt+space").unwrap());
let mut modes = config.modes;
if !modes.contains_key("default") {
modes.insert("Default".to_string(), "default".to_string());
}
- init_event_handler(sender, hotkey.map(|x| x.id));
+ init_event_handler(sender, shortcut);
Menu::with_items(&[
&MenuItem::with_id(
@@ -64,7 +66,7 @@ pub fn menu_builder(config: Config, sender: ExtSender, update_item: bool) -> Men
&open_github_item(),
&PredefinedMenuItem::separator(),
&refresh_item(),
- &open_item(hotkey),
+ &open_item(),
&mode_item(modes),
&PredefinedMenuItem::separator(),
&open_issue_item(),
@@ -86,10 +88,12 @@ fn get_image() -> DynamicImage {
.unwrap()
}
-fn init_event_handler(sender: ExtSender, hotkey_id: Option) {
+fn init_event_handler(sender: ExtSender, shortcut: Shortcut) {
let runtime = Runtime::new().unwrap();
+ let shortcut = shortcut.clone();
MenuEvent::set_event_handler(Some(move |x: MenuEvent| {
+ let shortcut = shortcut.clone();
let sender = sender.clone();
let sender = sender.0.clone();
info!("Menubar event called: {}", x.id.0);
@@ -107,11 +111,12 @@ fn init_event_handler(sender: ExtSender, hotkey_id: Option) {
open_url("https://github.com/RustCastLabs/rustcast/issues/new");
}
"show_rustcast" => {
- if let Some(hk) = hotkey_id {
- runtime.spawn(async move {
- sender.clone().try_send(Message::KeyPressed(hk)).unwrap();
- });
- }
+ runtime.spawn(async move {
+ sender
+ .clone()
+ .try_send(Message::KeyPressed(shortcut.clone()))
+ .unwrap();
+ });
}
"update" => {
open_url("https://github.com/RustCastLabs/rustcast/releases/latest");
@@ -178,13 +183,8 @@ fn mode_item(modes: HashMap) -> Submenu {
Submenu::with_items("Modes", true, &items).unwrap()
}
-fn open_item(hotkey: Option) -> MenuItem {
- MenuItem::with_id(
- "show_rustcast",
- "Toggle View",
- true,
- hotkey.map(|hk| Accelerator::new(Some(hk.mods), hk.key)),
- )
+fn open_item() -> MenuItem {
+ MenuItem::with_id("show_rustcast", "Toggle View", true, None)
}
fn open_github_item() -> MenuItem {
diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs
index 6a20ab0d..2c59db10 100644
--- a/src/app/pages/settings.rs
+++ b/src/app/pages/settings.rs
@@ -44,7 +44,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
.width(Length::Fill)
.style(move |_, _| settings_text_input_item_style(&hotkey_theme))
.into(),
- notice_item(theme.clone(), "Requires a restart"),
+ notice_item(theme.clone(), "Use \"+\" as a seperator"),
]);
let cb_theme = theme.clone();
@@ -56,7 +56,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
.width(Length::Fill)
.style(move |_, _| settings_text_input_item_style(&cb_theme))
.into(),
- notice_item(theme.clone(), "Requires a restart"),
+ notice_item(theme.clone(), "Use \"+\" as a seperator"),
]);
let placeholder_theme = theme.clone();
diff --git a/src/app/tile.rs b/src/app/tile.rs
index f842a1c0..a3c7b960 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -5,13 +5,12 @@ pub mod update;
use crate::app::apps::App;
use crate::app::{ArrowKey, Message, Move, Page};
use crate::clipboard::ClipBoardContentType;
-use crate::config::Config;
+use crate::config::{Config, Shelly};
use crate::debounce::Debouncer;
use crate::platform::default_app_paths;
+use crate::platform::macos::launching::Shortcut;
use arboard::Clipboard;
-use global_hotkey::hotkey::HotKey;
-use global_hotkey::{GlobalHotKeyEvent, HotKeyState};
use iced::futures::SinkExt;
use iced::futures::channel::mpsc::{Sender, channel};
@@ -190,9 +189,9 @@ pub struct Tile {
/// Stores the toggle [`HotKey`] and the Clipboard [`HotKey`]
#[derive(Clone, Debug)]
pub struct Hotkeys {
- pub toggle: HotKey,
- pub clipboard_hotkey: HotKey,
- pub shells: HashMap,
+ pub toggle: Shortcut,
+ pub clipboard_hotkey: Shortcut,
+ pub shells: HashMap,
}
impl Tile {
@@ -229,7 +228,6 @@ impl Tile {
_ => None,
});
Subscription::batch([
- Subscription::run(handle_hotkeys),
Subscription::run(handle_hot_reloading),
keyboard,
Subscription::run(handle_recipient),
@@ -269,9 +267,8 @@ impl Tile {
keyboard::Key::Named(Named::Backspace) => {
return Some(Message::FocusTextInput(Move::Back));
}
- _ => {}
+ _ => None,
}
- None
} else {
None
}
@@ -337,22 +334,6 @@ impl Tile {
}
}
-/// This is the subscription function that handles hotkeys, e.g. for hiding / showing the window
-fn handle_hotkeys() -> impl futures::Stream- {
- stream::channel(100, async |mut output| {
- let receiver = GlobalHotKeyEvent::receiver();
- loop {
- info!("Hotkey received");
- if let Ok(event) = receiver.recv()
- && event.state == HotKeyState::Pressed
- {
- output.try_send(Message::KeyPressed(event.id)).unwrap();
- }
- tokio::time::sleep(Duration::from_millis(100)).await;
- }
- })
-}
-
/// This is the subscription function that handles the change in clipboard history
fn handle_clipboard_history() -> impl futures::Stream
- {
stream::channel(100, async |mut output| {
@@ -602,7 +583,6 @@ fn handle_recipient() -> impl futures::Stream
- {
let abcd = recipient
.try_recv()
.map(async |msg| {
- info!("Sending a message");
output.send(msg).await.unwrap();
})
.ok();
diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs
index 03eca1d4..87fadf9c 100644
--- a/src/app/tile/elm.rs
+++ b/src/app/tile/elm.rs
@@ -4,7 +4,6 @@
use std::collections::HashMap;
use std::fs;
-use global_hotkey::hotkey::HotKey;
use iced::border::Radius;
use iced::widget::scrollable::{Anchor, Direction, Scrollbar};
use iced::widget::text::LineHeight;
@@ -35,7 +34,7 @@ use crate::{
};
/// Initialise the base window
-pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
+pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) {
let (id, open) = window::open(default_settings());
info!("Opening window");
@@ -60,24 +59,6 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
options.par_sort_by_key(|x| x.display_name.len());
let options = AppIndex::from_apps(options);
- let mut shells_map = HashMap::new();
- for shell in &config.shells {
- if let Some(hk_str) = &shell.hotkey
- && let Ok(hk) = hk_str.parse::()
- {
- shells_map.insert(hk.id, shell.command.clone());
- }
- }
-
- let hotkeys = Hotkeys {
- toggle: hotkey,
- clipboard_hotkey: config
- .clipboard_hotkey
- .parse()
- .unwrap_or("SUPER+SHIFT+C".parse().unwrap()),
- shells: shells_map,
- };
-
let home = std::env::var("HOME").unwrap_or("/".to_string());
let ranking = toml::from_str(
@@ -94,8 +75,8 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
focus_id: 0,
results: vec![],
options,
- emoji_apps: AppIndex::from_apps(App::emoji_apps()),
hotkeys,
+ emoji_apps: AppIndex::from_apps(App::emoji_apps()),
visible: true,
frontmost: None,
focused: false,
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index 4bca0409..356e0ea9 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -1,5 +1,6 @@
//! This handles the update logic for the tile (AKA rustcast's main window)
use std::cmp::min;
+use std::collections::HashMap;
use std::fs;
use std::io::Cursor;
use std::thread;
@@ -34,6 +35,8 @@ use crate::commands::Function;
use crate::config::Config;
use crate::config::MainPage;
use crate::debounce::DebouncePolicy;
+use crate::platform::macos::launching::Shortcut;
+use crate::platform::macos::launching::global_handler;
use crate::platform::macos::{start_at_login, stop_at_login};
use crate::quit::get_open_apps;
use crate::unit_conversion;
@@ -91,6 +94,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Message::SetSender(sender) => {
tile.sender = Some(sender.clone());
+ global_handler(sender.clone());
if tile.config.show_trayicon {
tile.tray_icon = Some(menu_icon(tile.config.clone(), sender));
}
@@ -274,6 +278,24 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Err(_) => return Task::none(),
};
+ if let Some(hotkey) = Shortcut::parse(&new_config.clipboard_hotkey).ok() {
+ tile.hotkeys.clipboard_hotkey = hotkey
+ }
+
+ if let Some(hotkey) = Shortcut::parse(&new_config.toggle_hotkey).ok() {
+ tile.hotkeys.toggle = hotkey
+ }
+
+ let mut shell_map = HashMap::new();
+
+ for shell in &new_config.shells {
+ if let Some(hotkey) = shell.hotkey.clone().and_then(|x| Shortcut::parse(&x).ok()) {
+ shell_map.insert(hotkey, shell.clone());
+ }
+ }
+
+ tile.hotkeys.shells = shell_map;
+
let update_apps_task = if tile.config.shells != new_config.shells {
info!("App Update required");
Task::done(Message::UpdateApps)
@@ -303,17 +325,20 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Task::batch([Task::done(Message::LoadRanking), update_apps_task])
}
- Message::KeyPressed(hk_id) => {
- if let Some(cmd) = tile.hotkeys.shells.get(&hk_id) {
- return Task::done(Message::RunFunction(Function::RunShellCommand(cmd.clone())));
+ Message::KeyPressed(shortcut) => {
+ if let Some(cmd) = tile.hotkeys.shells.get(&shortcut) {
+ return Task::done(Message::RunFunction(Function::RunShellCommand(
+ cmd.command.clone(),
+ )));
}
-
- let is_clipboard_hotkey = tile.hotkeys.clipboard_hotkey.id == hk_id;
- let is_open_hotkey = hk_id == tile.hotkeys.toggle.id;
+ let is_clipboard_hotkey = shortcut == tile.hotkeys.clipboard_hotkey;
+ let is_open_hotkey = shortcut == tile.hotkeys.toggle;
let clipboard_page_task = if is_clipboard_hotkey {
+ info!("Switching to clipboard page");
Task::done(Message::SwitchToPage(Page::ClipboardHistory))
} else if is_open_hotkey {
+ info!("Switching to main page");
Task::done(Message::SwitchToPage(Page::Main))
} else {
Task::none()
@@ -485,6 +510,18 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
new_options.par_sort_by_key(|x| x.display_name.len());
tile.options = AppIndex::from_apps(new_options);
+ let mut shell_map = HashMap::new();
+
+ for shell in &tile.config.shells {
+ if let Some(has_hk) = &shell.hotkey
+ && let Some(hotkey) = Shortcut::parse(has_hk).ok()
+ {
+ shell_map.insert(hotkey, shell.clone());
+ }
+ }
+
+ tile.hotkeys.shells = shell_map;
+
Task::none()
}
diff --git a/src/main.rs b/src/main.rs
index 6b06a7c4..82c88636 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,18 +12,14 @@ mod styles;
mod unit_conversion;
mod utils;
-use std::{fs::OpenOptions, path::Path};
+use std::{collections::HashMap, fs::OpenOptions, path::Path};
use crate::{
- app::tile::{self, Tile},
+ app::tile::{self, Hotkeys, Tile},
config::Config,
- platform::macos::get_autostart_status,
+ platform::macos::{get_autostart_status, launching::Shortcut},
};
-use global_hotkey::{
- GlobalHotKeyManager,
- hotkey::{Code, HotKey, Modifiers},
-};
use log::info;
use tracing_subscriber::{EnvFilter, Layer, util::SubscriberInitExt};
@@ -67,36 +63,33 @@ fn main() -> iced::Result {
info!("Config loaded");
- let manager = GlobalHotKeyManager::new().unwrap();
+ let show_hide =
+ Shortcut::parse(&config.toggle_hotkey).unwrap_or(Shortcut::parse("option+space").unwrap());
- let show_hide = config
- .toggle_hotkey
- .parse()
- .unwrap_or(HotKey::new(Some(Modifiers::ALT), Code::Space));
+ let cbhist = Shortcut::parse(&config.clipboard_hotkey.to_lowercase())
+ .unwrap_or_else(|_| Shortcut::parse("cmd+shift+c").unwrap());
- let cbhist = config
- .clipboard_hotkey
- .parse()
- .unwrap_or("SUPER+SHIFT+C".parse().unwrap());
+ let mut shell_map = HashMap::new();
- let mut hotkeys = vec![show_hide, cbhist];
for shell in &config.shells {
if let Some(hk_str) = &shell.hotkey
- && let Ok(hk) = hk_str.parse::()
+ && let Ok(hk) = Shortcut::parse(hk_str)
{
- hotkeys.push(hk);
+ shell_map.insert(hk, shell.clone());
}
}
- manager
- .register_all(&hotkeys)
- .expect("Unable to register hotkeys");
+ let hotkeys = Hotkeys {
+ toggle: show_hide,
+ clipboard_hotkey: cbhist,
+ shells: shell_map,
+ };
info!("Hotkeys loaded");
info!("Starting rustcast");
iced::daemon(
- move || tile::elm::new(show_hide, &config),
+ move || tile::elm::new(hotkeys.clone(), &config),
tile::update::handle_update,
tile::elm::view,
)
diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs
new file mode 100644
index 00000000..fd8c55b8
--- /dev/null
+++ b/src/platform/macos/launching.rs
@@ -0,0 +1,250 @@
+use std::sync::{Arc, Mutex};
+
+use block2::RcBlock;
+use objc2_app_kit::{NSEvent, NSEventMask, NSEventModifierFlags, NSEventType};
+
+use crate::app::{Message, tile::ExtSender};
+
+pub fn global_handler(sender: ExtSender) {
+ local_handler(sender.clone());
+ let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged;
+ let sender = Arc::new(Mutex::new(sender.0.clone()));
+
+ let block = RcBlock::new({
+ move |event: std::ptr::NonNull| {
+ let event = unsafe { event.as_ref() };
+ let event_type = event.r#type();
+
+ let key_code = event.keyCode();
+ let mods = event.modifierFlags()
+ & (NSEventModifierFlags::Command
+ | NSEventModifierFlags::Option
+ | NSEventModifierFlags::Control
+ | NSEventModifierFlags::Function
+ | NSEventModifierFlags::CapsLock
+ | NSEventModifierFlags::Shift);
+
+ let shortcut = match event_type {
+ NSEventType::KeyDown => Shortcut {
+ key_code: Some(key_code),
+ mods: if mods.0 != 0 {
+ Some(mods.0 as usize)
+ } else {
+ None
+ },
+ },
+ NSEventType::FlagsChanged => Shortcut {
+ key_code: None,
+ mods: if mods.0 != 0 {
+ Some(mods.0 as usize)
+ } else {
+ None
+ },
+ },
+ _ => return,
+ };
+
+ let mut s = sender.lock().unwrap();
+ let _ = s.try_send(Message::KeyPressed(shortcut));
+ }
+ });
+
+ NSEvent::addGlobalMonitorForEventsMatchingMask_handler(mask, &block);
+}
+
+pub fn local_handler(sender: ExtSender) {
+ let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged;
+ let sender = Arc::new(Mutex::new(sender.0.clone()));
+
+ let block = RcBlock::new({
+ move |event: std::ptr::NonNull| -> *mut NSEvent {
+ let event_ref = unsafe { event.as_ref() };
+ let event_type = event_ref.r#type();
+
+ let key_code = event_ref.keyCode();
+ let mods = event_ref.modifierFlags()
+ & (NSEventModifierFlags::Command
+ | NSEventModifierFlags::Option
+ | NSEventModifierFlags::Control
+ | NSEventModifierFlags::Function
+ | NSEventModifierFlags::CapsLock
+ | NSEventModifierFlags::Shift);
+
+ let shortcut = match event_type {
+ NSEventType::KeyDown => Shortcut {
+ key_code: Some(key_code),
+ mods: if mods.0 != 0 {
+ Some(mods.0 as usize)
+ } else {
+ None
+ },
+ },
+ NSEventType::FlagsChanged => Shortcut {
+ key_code: None,
+ mods: if mods.0 != 0 {
+ Some(mods.0 as usize)
+ } else {
+ None
+ },
+ },
+ _ => return event.as_ptr(), // pass through unhandled events
+ };
+
+ let mut s = sender.lock().unwrap();
+ let _ = s.try_send(Message::KeyPressed(shortcut));
+
+ event.as_ptr()
+ }
+ });
+
+ unsafe {
+ NSEvent::addLocalMonitorForEventsMatchingMask_handler(mask, &block);
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Shortcut {
+ pub key_code: Option,
+ pub mods: Option,
+}
+
+impl Shortcut {
+ pub fn new(key_code: Option, mods: Option) -> Self {
+ Self { key_code, mods }
+ }
+
+ pub fn parse(s: &str) -> Result {
+ let parts: Vec<&str> = s.split('+').map(|p| p.trim()).collect();
+
+ let mut mods: usize = 0;
+ let mut key_code: Option = None;
+ let mut has_mods = false;
+
+ for part in &parts {
+ match part.to_lowercase().as_str() {
+ "cmd" | "command" | "super" => {
+ mods |= NSEventModifierFlags::Command.0;
+ has_mods = true;
+ }
+ "opt" | "option" | "alt" => {
+ mods |= NSEventModifierFlags::Option.0;
+ has_mods = true;
+ }
+ "capslock" | "caps" | "caps lock" => mods |= NSEventModifierFlags::CapsLock.0,
+ "ctrl" | "control" => {
+ mods |= NSEventModifierFlags::Control.0;
+ has_mods = true;
+ }
+ "shift" => {
+ mods |= NSEventModifierFlags::Shift.0;
+ has_mods = true;
+ }
+ "fn" | "function" => {
+ mods |= NSEventModifierFlags::Function.0;
+ has_mods = true;
+ }
+ key => {
+ if key_code.is_some() {
+ return Err(format!("Multiple keys specified: '{}'", s));
+ }
+ key_code = Some(str_to_keycode(key)?);
+ }
+ }
+ }
+
+ Ok(Shortcut::new(
+ key_code,
+ if has_mods { Some(mods) } else { None },
+ ))
+ }
+}
+
+fn str_to_keycode(s: &str) -> Result {
+ let code = match s.to_lowercase().as_str() {
+ // Letters
+ "a" => 0x00,
+ "s" => 0x01,
+ "d" => 0x02,
+ "f" => 0x03,
+ "h" => 0x04,
+ "g" => 0x05,
+ "z" => 0x06,
+ "x" => 0x07,
+ "c" => 0x08,
+ "v" => 0x09,
+ "b" => 0x0b,
+ "q" => 0x0c,
+ "w" => 0x0d,
+ "e" => 0x0e,
+ "r" => 0x0f,
+ "y" => 0x10,
+ "t" => 0x11,
+ "o" => 0x1f,
+ "u" => 0x20,
+ "i" => 0x22,
+ "p" => 0x23,
+ "l" => 0x25,
+ "j" => 0x26,
+ "k" => 0x28,
+ "n" => 0x2d,
+ "m" => 0x2e,
+
+ // Numbers
+ "1" => 0x12,
+ "2" => 0x13,
+ "3" => 0x14,
+ "4" => 0x15,
+ "5" => 0x17,
+ "6" => 0x16,
+ "7" => 0x1a,
+ "8" => 0x1c,
+ "9" => 0x19,
+ "0" => 0x1d,
+
+ // Special keys
+ "return" | "enter" => 0x24,
+ "tab" => 0x30,
+ "space" => 0x31,
+ "delete" | "backspace" => 0x33,
+ "escape" | "esc" => 0x35,
+ "left" | "arrowleft" => 0x7b,
+ "right" | "arrowright" => 0x7c,
+ "down" | "arrowdown" => 0x7d,
+ "up" | "arrowup" => 0x7e,
+ "home" => 0x73,
+ "end" => 0x77,
+ "pageup" => 0x74,
+ "pagedown" => 0x79,
+
+ // Function keys
+ "f1" => 0x7a,
+ "f2" => 0x78,
+ "f3" => 0x63,
+ "f4" => 0x76,
+ "f5" => 0x60,
+ "f6" => 0x61,
+ "f7" => 0x62,
+ "f8" => 0x64,
+ "f9" => 0x65,
+ "f10" => 0x6d,
+ "f11" => 0x67,
+ "f12" => 0x6f,
+
+ // Symbols
+ "-" | "minus" => 0x1b,
+ "=" | "equal" => 0x18,
+ "[" | "bracketleft" => 0x21,
+ "]" | "bracketright" => 0x1e,
+ "\\" | "backslash" => 0x2a,
+ ";" | "semicolon" => 0x29,
+ "'" | "quote" => 0x27,
+ "`" | "backquote" | "grave" => 0x32,
+ "," | "comma" => 0x2b,
+ "." | "period" => 0x2f,
+ "/" | "slash" => 0x2c,
+
+ _ => return Err(format!("Unknown key: '{}'", s)),
+ };
+
+ Ok(code)
+}
diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs
index ab7c05c0..eeab1289 100644
--- a/src/platform/macos/mod.rs
+++ b/src/platform/macos/mod.rs
@@ -1,6 +1,7 @@
//! Macos specific logic, such as window settings, etc.
pub mod discovery;
pub mod haptics;
+pub mod launching;
use iced::wgpu::rwh::WindowHandle;
From 66cb6c70825a8e0b74701b95550dc7668a569e9f Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sun, 29 Mar 2026 17:36:55 +0800
Subject: [PATCH 29/51] clippy & format -_-
---
src/app/tile.rs | 22 ++++++++++------------
src/app/tile/update.rs | 4 ++--
src/platform/macos/launching.rs | 24 ++++--------------------
3 files changed, 16 insertions(+), 34 deletions(-)
diff --git a/src/app/tile.rs b/src/app/tile.rs
index a3c7b960..02e029a9 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -239,33 +239,31 @@ impl Tile {
if let keyboard::Event::KeyPressed { key, modifiers, .. } = event {
match key {
keyboard::Key::Named(Named::ArrowUp) => {
- return Some(Message::ChangeFocus(ArrowKey::Up, 1));
+ Some(Message::ChangeFocus(ArrowKey::Up, 1))
}
keyboard::Key::Named(Named::ArrowLeft) => {
- return Some(Message::ChangeFocus(ArrowKey::Left, 1));
+ Some(Message::ChangeFocus(ArrowKey::Left, 1))
}
keyboard::Key::Named(Named::ArrowRight) => {
- return Some(Message::ChangeFocus(ArrowKey::Right, 1));
+ Some(Message::ChangeFocus(ArrowKey::Right, 1))
}
keyboard::Key::Named(Named::ArrowDown) => {
- return Some(Message::ChangeFocus(ArrowKey::Down, 1));
+ Some(Message::ChangeFocus(ArrowKey::Down, 1))
}
keyboard::Key::Character(chr) => {
if modifiers.command() && chr.to_string() == "r" {
- return Some(Message::ReloadConfig);
+ Some(Message::ReloadConfig)
} else if chr.to_string() == "p" && modifiers.control() {
- return Some(Message::ChangeFocus(ArrowKey::Up, 1));
+ Some(Message::ChangeFocus(ArrowKey::Up, 1))
} else if chr.to_string() == "n" && modifiers.control() {
- return Some(Message::ChangeFocus(ArrowKey::Down, 1));
+ Some(Message::ChangeFocus(ArrowKey::Down, 1))
} else {
- return Some(Message::FocusTextInput(Move::Forwards(
- chr.to_string(),
- )));
+ Some(Message::FocusTextInput(Move::Forwards(chr.to_string())))
}
}
- keyboard::Key::Named(Named::Enter) => return Some(Message::OpenFocused),
+ keyboard::Key::Named(Named::Enter) => Some(Message::OpenFocused),
keyboard::Key::Named(Named::Backspace) => {
- return Some(Message::FocusTextInput(Move::Back));
+ Some(Message::FocusTextInput(Move::Back))
}
_ => None,
}
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index 356e0ea9..9ae79519 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -278,11 +278,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Err(_) => return Task::none(),
};
- if let Some(hotkey) = Shortcut::parse(&new_config.clipboard_hotkey).ok() {
+ if let Ok(hotkey) = Shortcut::parse(&new_config.clipboard_hotkey) {
tile.hotkeys.clipboard_hotkey = hotkey
}
- if let Some(hotkey) = Shortcut::parse(&new_config.toggle_hotkey).ok() {
+ if let Ok(hotkey) = Shortcut::parse(&new_config.toggle_hotkey) {
tile.hotkeys.toggle = hotkey
}
diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs
index fd8c55b8..5ef49b4a 100644
--- a/src/platform/macos/launching.rs
+++ b/src/platform/macos/launching.rs
@@ -27,19 +27,11 @@ pub fn global_handler(sender: ExtSender) {
let shortcut = match event_type {
NSEventType::KeyDown => Shortcut {
key_code: Some(key_code),
- mods: if mods.0 != 0 {
- Some(mods.0 as usize)
- } else {
- None
- },
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
},
NSEventType::FlagsChanged => Shortcut {
key_code: None,
- mods: if mods.0 != 0 {
- Some(mods.0 as usize)
- } else {
- None
- },
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
},
_ => return,
};
@@ -73,19 +65,11 @@ pub fn local_handler(sender: ExtSender) {
let shortcut = match event_type {
NSEventType::KeyDown => Shortcut {
key_code: Some(key_code),
- mods: if mods.0 != 0 {
- Some(mods.0 as usize)
- } else {
- None
- },
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
},
NSEventType::FlagsChanged => Shortcut {
key_code: None,
- mods: if mods.0 != 0 {
- Some(mods.0 as usize)
- } else {
- None
- },
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
},
_ => return event.as_ptr(), // pass through unhandled events
};
From ebf2520d05d48a926798a500f8ac128e0e9d930f Mon Sep 17 00:00:00 2001
From: niklasmarderx
Date: Wed, 1 Apr 2026 09:41:55 +0200
Subject: [PATCH 30/51] fix: highlight favourite icon and sort favourites
alphabetically
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The heart icon now shows ❤️ for favourites and 🤍 for non-favourites,
with brighter text when the app is marked as a favourite. Previously
the icon looked the same regardless of favourite status.
Favourites are now sorted alphabetically by display name.
Closes #254
---
src/app/apps.rs | 6 ++++--
src/app/tile.rs | 16 +++++++---------
src/styles.rs | 23 +++++++++++++++++------
3 files changed, 28 insertions(+), 17 deletions(-)
diff --git a/src/app/apps.rs b/src/app/apps.rs
index 7b6624d5..17227773 100644
--- a/src/app/apps.rs
+++ b/src/app/apps.rs
@@ -213,11 +213,13 @@ impl App {
let name = self.search_name.clone();
let theme_clone = theme.clone();
+ let is_favourite = self.ranking == -1;
+ let heart = if is_favourite { "❤️" } else { "🤍" };
row = row.push(
- Button::new(Text::new("♥️").width(Length::Fill).align_x(Alignment::End))
+ Button::new(Text::new(heart).width(Length::Fill).align_x(Alignment::End))
.on_press_with(move || Message::ToggleFavouriteApp(name.clone()))
.width(Length::Fill)
- .style(move |_, status| favourite_button_style(&theme_clone, status)),
+ .style(move |_, status| favourite_button_style(&theme_clone, status, is_favourite)),
);
let msg = on_press.or(match self.open_command.clone() {
diff --git a/src/app/tile.rs b/src/app/tile.rs
index 02e029a9..05c41668 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -109,16 +109,14 @@ impl AppIndex {
}
fn get_favourites(&self) -> Vec {
- self.by_name
+ let mut favs: Vec = self
+ .by_name
.values()
- .filter_map(|x| {
- if x.ranking == -1 {
- Some(x.to_owned())
- } else {
- None
- }
- })
- .collect()
+ .filter(|x| x.ranking == -1)
+ .cloned()
+ .collect();
+ favs.sort_by(|a, b| a.display_name.cmp(&b.display_name));
+ favs
}
fn empty() -> AppIndex {
diff --git a/src/styles.rs b/src/styles.rs
index 3972b9a3..a978adbc 100644
--- a/src/styles.rs
+++ b/src/styles.rs
@@ -75,12 +75,23 @@ pub fn result_button_style(theme: &ConfigTheme) -> button::Style {
}
}
-pub fn favourite_button_style(theme: &ConfigTheme, status: button::Status) -> button::Style {
- let text_color = match status {
- button::Status::Pressed => theme.text_color(1.),
- button::Status::Hovered => theme.text_color(0.5),
- button::Status::Active => theme.text_color(0.1),
- button::Status::Disabled => theme.text_color(0.1),
+pub fn favourite_button_style(
+ theme: &ConfigTheme,
+ status: button::Status,
+ is_favourite: bool,
+) -> button::Style {
+ let text_color = if is_favourite {
+ match status {
+ button::Status::Pressed => theme.text_color(0.8),
+ button::Status::Hovered => theme.text_color(0.9),
+ _ => theme.text_color(1.),
+ }
+ } else {
+ match status {
+ button::Status::Pressed => theme.text_color(1.),
+ button::Status::Hovered => theme.text_color(0.5),
+ _ => theme.text_color(0.1),
+ }
};
button::Style {
From 5a0a12379a4268145bf990568646c6d34b06b8dd Mon Sep 17 00:00:00 2001
From: niklasmarderx
Date: Wed, 1 Apr 2026 10:54:03 +0200
Subject: [PATCH 31/51] fix: address review feedback - simplify style, drop
emoji swap
---
src/app/apps.rs | 3 +--
src/styles.rs | 20 +++++++++-----------
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/src/app/apps.rs b/src/app/apps.rs
index 17227773..90d615cc 100644
--- a/src/app/apps.rs
+++ b/src/app/apps.rs
@@ -214,9 +214,8 @@ impl App {
let name = self.search_name.clone();
let theme_clone = theme.clone();
let is_favourite = self.ranking == -1;
- let heart = if is_favourite { "❤️" } else { "🤍" };
row = row.push(
- Button::new(Text::new(heart).width(Length::Fill).align_x(Alignment::End))
+ Button::new(Text::new("♥️").width(Length::Fill).align_x(Alignment::End))
.on_press_with(move || Message::ToggleFavouriteApp(name.clone()))
.width(Length::Fill)
.style(move |_, status| favourite_button_style(&theme_clone, status, is_favourite)),
diff --git a/src/styles.rs b/src/styles.rs
index a978adbc..91861f88 100644
--- a/src/styles.rs
+++ b/src/styles.rs
@@ -80,18 +80,16 @@ pub fn favourite_button_style(
status: button::Status,
is_favourite: bool,
) -> button::Style {
- let text_color = if is_favourite {
- match status {
- button::Status::Pressed => theme.text_color(0.8),
- button::Status::Hovered => theme.text_color(0.9),
- _ => theme.text_color(1.),
- }
+ let (base, pressed, hovered) = if is_favourite {
+ (1.0, 0.8, 0.9)
} else {
- match status {
- button::Status::Pressed => theme.text_color(1.),
- button::Status::Hovered => theme.text_color(0.5),
- _ => theme.text_color(0.1),
- }
+ (0.1, 1.0, 0.5)
+ };
+
+ let text_color = match status {
+ button::Status::Pressed => theme.text_color(pressed),
+ button::Status::Hovered => theme.text_color(hovered),
+ _ => theme.text_color(base),
};
button::Style {
From 050933178e5a0e6fa3bbb0cdb27e5fcd5c2f78fd Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Thu, 2 Apr 2026 15:04:19 +0800
Subject: [PATCH 32/51] Remove manual input handling
---
Cargo.lock | 18 ---
Cargo.toml | 6 +-
src/app.rs | 3 +-
src/app/menubar.rs | 52 +++----
src/app/pages/settings.rs | 4 +-
src/app/tile.rs | 54 +++-----
src/app/tile/elm.rs | 23 +---
src/app/tile/update.rs | 48 ++++++-
src/main.rs | 39 +++---
src/platform/macos/launching.rs | 234 ++++++++++++++++++++++++++++++++
src/platform/macos/mod.rs | 1 +
11 files changed, 336 insertions(+), 146 deletions(-)
create mode 100644 src/platform/macos/launching.rs
diff --git a/Cargo.lock b/Cargo.lock
index c059a04c..98cf30de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1571,23 +1571,6 @@ dependencies = [
"system-deps",
]
-[[package]]
-name = "global-hotkey"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
-dependencies = [
- "crossbeam-channel",
- "keyboard-types",
- "objc2 0.6.3",
- "objc2-app-kit 0.3.2",
- "once_cell",
- "thiserror 2.0.18",
- "windows-sys 0.59.0",
- "x11rb",
- "xkeysym",
-]
-
[[package]]
name = "glow"
version = "0.16.0"
@@ -3856,7 +3839,6 @@ dependencies = [
"arboard",
"block2 0.6.2",
"emojis",
- "global-hotkey",
"iced",
"icns",
"image",
diff --git a/Cargo.toml b/Cargo.toml
index 82cb2404..43950c10 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,6 @@ repository = "https://github.com/RustCastLabs/rustcast"
arboard = "3.6.1"
block2 = "0.6.2"
emojis = "0.8.0"
-global-hotkey = "0.7.0"
iced = { version = "0.14.0", features = ["image", "tokio"] }
icns = "0.3.1"
image = { version = "0.25.9", features = ["tiff"] }
@@ -20,10 +19,7 @@ log = "0.4.29"
minreq = { version = "2.14.1", features = ["https"] }
objc2 = "0.6.3"
objc2-app-kit = { version = "0.3.2", features = ["NSImage"] }
-objc2-application-services = { version = "0.3.2", default-features = false, features = [
- "HIServices",
- "Processes",
-] }
+objc2-application-services = { version = "0.3.2", default-features = false, features = ["HIServices", "Processes"] }
objc2-core-foundation = "0.3.2"
objc2-foundation = { version = "0.3.2", features = ["NSString"] }
objc2-service-management = "0.3.2"
diff --git a/src/app.rs b/src/app.rs
index 2be7378c..68b19e41 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,6 +5,7 @@ use crate::app::apps::{App, AppCommand, ICNS_ICON};
use crate::commands::Function;
use crate::config::{Config, MainPage, Shelly};
use crate::debounce::DebouncePolicy;
+use crate::platform::macos::launching::Shortcut;
use crate::utils::icns_data_to_handle;
use crate::{app::tile::ExtSender, clipboard::ClipBoardContentType};
use iced::time::Duration;
@@ -90,7 +91,7 @@ pub enum Message {
OpenResult(u32),
OpenToSettings,
SearchQueryChanged(String, Id),
- KeyPressed(u32),
+ KeyPressed(Shortcut),
FocusTextInput(Move),
HideWindow(Id),
RunFunction(Function),
diff --git a/src/app/menubar.rs b/src/app/menubar.rs
index b9a566f6..7b82e887 100644
--- a/src/app/menubar.rs
+++ b/src/app/menubar.rs
@@ -2,20 +2,20 @@
use std::{collections::HashMap, io::Cursor};
-use global_hotkey::hotkey::{Code, HotKey, Modifiers};
use image::{DynamicImage, ImageReader};
use log::info;
use tray_icon::{
Icon, TrayIcon, TrayIconBuilder,
menu::{
AboutMetadataBuilder, Icon as Ico, IsMenuItem, Menu, MenuEvent, MenuItem,
- PredefinedMenuItem, Submenu, accelerator::Accelerator,
+ PredefinedMenuItem, Submenu,
},
};
use crate::{
app::{Message, tile::ExtSender},
config::Config,
+ platform::macos::launching::Shortcut,
utils::open_url,
};
@@ -39,14 +39,15 @@ pub fn menu_icon(config: Config, sender: ExtSender) -> TrayIcon {
}
pub fn menu_builder(config: Config, sender: ExtSender, update_item: bool) -> Menu {
- let hotkey = config.toggle_hotkey.parse::().ok();
+ let shortcut =
+ Shortcut::parse(&config.toggle_hotkey).unwrap_or(Shortcut::parse("opt+space").unwrap());
let mut modes = config.modes;
if !modes.contains_key("default") {
modes.insert("Default".to_string(), "default".to_string());
}
- init_event_handler(sender, hotkey.map(|x| x.id));
+ init_event_handler(sender, shortcut);
Menu::with_items(&[
&MenuItem::with_id(
@@ -64,7 +65,7 @@ pub fn menu_builder(config: Config, sender: ExtSender, update_item: bool) -> Men
&open_github_item(),
&PredefinedMenuItem::separator(),
&refresh_item(),
- &open_item(hotkey),
+ &open_item(),
&mode_item(modes),
&PredefinedMenuItem::separator(),
&open_issue_item(),
@@ -86,10 +87,12 @@ fn get_image() -> DynamicImage {
.unwrap()
}
-fn init_event_handler(sender: ExtSender, hotkey_id: Option) {
+fn init_event_handler(sender: ExtSender, shortcut: Shortcut) {
let runtime = Runtime::new().unwrap();
+ let shortcut = shortcut.clone();
MenuEvent::set_event_handler(Some(move |x: MenuEvent| {
+ let shortcut = shortcut.clone();
let sender = sender.clone();
let sender = sender.0.clone();
info!("Menubar event called: {}", x.id.0);
@@ -107,11 +110,12 @@ fn init_event_handler(sender: ExtSender, hotkey_id: Option) {
open_url("https://github.com/RustCastLabs/rustcast/issues/new");
}
"show_rustcast" => {
- if let Some(hk) = hotkey_id {
- runtime.spawn(async move {
- sender.clone().try_send(Message::KeyPressed(hk)).unwrap();
- });
- }
+ runtime.spawn(async move {
+ sender
+ .clone()
+ .try_send(Message::KeyPressed(shortcut.clone()))
+ .unwrap();
+ });
}
"update" => {
open_url("https://github.com/RustCastLabs/rustcast/releases/latest");
@@ -178,13 +182,8 @@ fn mode_item(modes: HashMap) -> Submenu {
Submenu::with_items("Modes", true, &items).unwrap()
}
-fn open_item(hotkey: Option) -> MenuItem {
- MenuItem::with_id(
- "show_rustcast",
- "Toggle View",
- true,
- hotkey.map(|hk| Accelerator::new(Some(hk.mods), hk.key)),
- )
+fn open_item() -> MenuItem {
+ MenuItem::with_id("show_rustcast", "Toggle View", true, None)
}
fn open_github_item() -> MenuItem {
@@ -196,24 +195,11 @@ fn open_issue_item() -> MenuItem {
}
fn refresh_item() -> MenuItem {
- MenuItem::with_id(
- "refresh_rustcast",
- "Refresh",
- true,
- Some(Accelerator::new(
- Some(Modifiers::SUPER),
- global_hotkey::hotkey::Code::KeyR,
- )),
- )
+ MenuItem::with_id("refresh_rustcast", "Refresh", true, None)
}
fn open_settings_item() -> MenuItem {
- MenuItem::with_id(
- "open_preferences",
- "Open Preferences",
- true,
- Some(Accelerator::new(Some(Modifiers::SUPER), Code::Comma)),
- )
+ MenuItem::with_id("open_preferences", "Open Preferences", true, None)
}
fn get_help_item() -> MenuItem {
diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs
index 6a20ab0d..2c59db10 100644
--- a/src/app/pages/settings.rs
+++ b/src/app/pages/settings.rs
@@ -44,7 +44,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
.width(Length::Fill)
.style(move |_, _| settings_text_input_item_style(&hotkey_theme))
.into(),
- notice_item(theme.clone(), "Requires a restart"),
+ notice_item(theme.clone(), "Use \"+\" as a seperator"),
]);
let cb_theme = theme.clone();
@@ -56,7 +56,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
.width(Length::Fill)
.style(move |_, _| settings_text_input_item_style(&cb_theme))
.into(),
- notice_item(theme.clone(), "Requires a restart"),
+ notice_item(theme.clone(), "Use \"+\" as a seperator"),
]);
let placeholder_theme = theme.clone();
diff --git a/src/app/tile.rs b/src/app/tile.rs
index f842a1c0..02e029a9 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -5,13 +5,12 @@ pub mod update;
use crate::app::apps::App;
use crate::app::{ArrowKey, Message, Move, Page};
use crate::clipboard::ClipBoardContentType;
-use crate::config::Config;
+use crate::config::{Config, Shelly};
use crate::debounce::Debouncer;
use crate::platform::default_app_paths;
+use crate::platform::macos::launching::Shortcut;
use arboard::Clipboard;
-use global_hotkey::hotkey::HotKey;
-use global_hotkey::{GlobalHotKeyEvent, HotKeyState};
use iced::futures::SinkExt;
use iced::futures::channel::mpsc::{Sender, channel};
@@ -190,9 +189,9 @@ pub struct Tile {
/// Stores the toggle [`HotKey`] and the Clipboard [`HotKey`]
#[derive(Clone, Debug)]
pub struct Hotkeys {
- pub toggle: HotKey,
- pub clipboard_hotkey: HotKey,
- pub shells: HashMap,
+ pub toggle: Shortcut,
+ pub clipboard_hotkey: Shortcut,
+ pub shells: HashMap,
}
impl Tile {
@@ -229,7 +228,6 @@ impl Tile {
_ => None,
});
Subscription::batch([
- Subscription::run(handle_hotkeys),
Subscription::run(handle_hot_reloading),
keyboard,
Subscription::run(handle_recipient),
@@ -241,37 +239,34 @@ impl Tile {
if let keyboard::Event::KeyPressed { key, modifiers, .. } = event {
match key {
keyboard::Key::Named(Named::ArrowUp) => {
- return Some(Message::ChangeFocus(ArrowKey::Up, 1));
+ Some(Message::ChangeFocus(ArrowKey::Up, 1))
}
keyboard::Key::Named(Named::ArrowLeft) => {
- return Some(Message::ChangeFocus(ArrowKey::Left, 1));
+ Some(Message::ChangeFocus(ArrowKey::Left, 1))
}
keyboard::Key::Named(Named::ArrowRight) => {
- return Some(Message::ChangeFocus(ArrowKey::Right, 1));
+ Some(Message::ChangeFocus(ArrowKey::Right, 1))
}
keyboard::Key::Named(Named::ArrowDown) => {
- return Some(Message::ChangeFocus(ArrowKey::Down, 1));
+ Some(Message::ChangeFocus(ArrowKey::Down, 1))
}
keyboard::Key::Character(chr) => {
if modifiers.command() && chr.to_string() == "r" {
- return Some(Message::ReloadConfig);
+ Some(Message::ReloadConfig)
} else if chr.to_string() == "p" && modifiers.control() {
- return Some(Message::ChangeFocus(ArrowKey::Up, 1));
+ Some(Message::ChangeFocus(ArrowKey::Up, 1))
} else if chr.to_string() == "n" && modifiers.control() {
- return Some(Message::ChangeFocus(ArrowKey::Down, 1));
+ Some(Message::ChangeFocus(ArrowKey::Down, 1))
} else {
- return Some(Message::FocusTextInput(Move::Forwards(
- chr.to_string(),
- )));
+ Some(Message::FocusTextInput(Move::Forwards(chr.to_string())))
}
}
- keyboard::Key::Named(Named::Enter) => return Some(Message::OpenFocused),
+ keyboard::Key::Named(Named::Enter) => Some(Message::OpenFocused),
keyboard::Key::Named(Named::Backspace) => {
- return Some(Message::FocusTextInput(Move::Back));
+ Some(Message::FocusTextInput(Move::Back))
}
- _ => {}
+ _ => None,
}
- None
} else {
None
}
@@ -337,22 +332,6 @@ impl Tile {
}
}
-/// This is the subscription function that handles hotkeys, e.g. for hiding / showing the window
-fn handle_hotkeys() -> impl futures::Stream
- {
- stream::channel(100, async |mut output| {
- let receiver = GlobalHotKeyEvent::receiver();
- loop {
- info!("Hotkey received");
- if let Ok(event) = receiver.recv()
- && event.state == HotKeyState::Pressed
- {
- output.try_send(Message::KeyPressed(event.id)).unwrap();
- }
- tokio::time::sleep(Duration::from_millis(100)).await;
- }
- })
-}
-
/// This is the subscription function that handles the change in clipboard history
fn handle_clipboard_history() -> impl futures::Stream
- {
stream::channel(100, async |mut output| {
@@ -602,7 +581,6 @@ fn handle_recipient() -> impl futures::Stream
- {
let abcd = recipient
.try_recv()
.map(async |msg| {
- info!("Sending a message");
output.send(msg).await.unwrap();
})
.ok();
diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs
index 03eca1d4..87fadf9c 100644
--- a/src/app/tile/elm.rs
+++ b/src/app/tile/elm.rs
@@ -4,7 +4,6 @@
use std::collections::HashMap;
use std::fs;
-use global_hotkey::hotkey::HotKey;
use iced::border::Radius;
use iced::widget::scrollable::{Anchor, Direction, Scrollbar};
use iced::widget::text::LineHeight;
@@ -35,7 +34,7 @@ use crate::{
};
/// Initialise the base window
-pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
+pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) {
let (id, open) = window::open(default_settings());
info!("Opening window");
@@ -60,24 +59,6 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
options.par_sort_by_key(|x| x.display_name.len());
let options = AppIndex::from_apps(options);
- let mut shells_map = HashMap::new();
- for shell in &config.shells {
- if let Some(hk_str) = &shell.hotkey
- && let Ok(hk) = hk_str.parse::()
- {
- shells_map.insert(hk.id, shell.command.clone());
- }
- }
-
- let hotkeys = Hotkeys {
- toggle: hotkey,
- clipboard_hotkey: config
- .clipboard_hotkey
- .parse()
- .unwrap_or("SUPER+SHIFT+C".parse().unwrap()),
- shells: shells_map,
- };
-
let home = std::env::var("HOME").unwrap_or("/".to_string());
let ranking = toml::from_str(
@@ -94,8 +75,8 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task) {
focus_id: 0,
results: vec![],
options,
- emoji_apps: AppIndex::from_apps(App::emoji_apps()),
hotkeys,
+ emoji_apps: AppIndex::from_apps(App::emoji_apps()),
visible: true,
frontmost: None,
focused: false,
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index 4bca0409..f3aa95b2 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -1,5 +1,6 @@
//! This handles the update logic for the tile (AKA rustcast's main window)
use std::cmp::min;
+use std::collections::HashMap;
use std::fs;
use std::io::Cursor;
use std::thread;
@@ -34,6 +35,8 @@ use crate::commands::Function;
use crate::config::Config;
use crate::config::MainPage;
use crate::debounce::DebouncePolicy;
+use crate::platform::macos::launching::Shortcut;
+use crate::platform::macos::launching::global_handler;
use crate::platform::macos::{start_at_login, stop_at_login};
use crate::quit::get_open_apps;
use crate::unit_conversion;
@@ -91,6 +94,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Message::SetSender(sender) => {
tile.sender = Some(sender.clone());
+ global_handler(sender.clone());
if tile.config.show_trayicon {
tile.tray_icon = Some(menu_icon(tile.config.clone(), sender));
}
@@ -274,6 +278,24 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Err(_) => return Task::none(),
};
+ if let Ok(hotkey) = Shortcut::parse(&new_config.clipboard_hotkey) {
+ tile.hotkeys.clipboard_hotkey = hotkey
+ }
+
+ if let Ok(hotkey) = Shortcut::parse(&new_config.toggle_hotkey) {
+ tile.hotkeys.toggle = hotkey
+ }
+
+ let mut shell_map = HashMap::new();
+
+ for shell in &new_config.shells {
+ if let Some(hotkey) = shell.hotkey.clone().and_then(|x| Shortcut::parse(&x).ok()) {
+ shell_map.insert(hotkey, shell.clone());
+ }
+ }
+
+ tile.hotkeys.shells = shell_map;
+
let update_apps_task = if tile.config.shells != new_config.shells {
info!("App Update required");
Task::done(Message::UpdateApps)
@@ -303,17 +325,21 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Task::batch([Task::done(Message::LoadRanking), update_apps_task])
}
- Message::KeyPressed(hk_id) => {
- if let Some(cmd) = tile.hotkeys.shells.get(&hk_id) {
- return Task::done(Message::RunFunction(Function::RunShellCommand(cmd.clone())));
+ Message::KeyPressed(shortcut) => {
+ if let Some(cmd) = tile.hotkeys.shells.get(&shortcut) {
+ return Task::done(Message::RunFunction(Function::RunShellCommand(
+ cmd.command.clone(),
+ )));
}
- let is_clipboard_hotkey = tile.hotkeys.clipboard_hotkey.id == hk_id;
- let is_open_hotkey = hk_id == tile.hotkeys.toggle.id;
+ let is_clipboard_hotkey = shortcut == tile.hotkeys.clipboard_hotkey;
+ let is_open_hotkey = shortcut == tile.hotkeys.toggle;
let clipboard_page_task = if is_clipboard_hotkey {
+ info!("Switching to clipboard page");
Task::done(Message::SwitchToPage(Page::ClipboardHistory))
} else if is_open_hotkey {
+ info!("Switching to main page");
Task::done(Message::SwitchToPage(Page::Main))
} else {
Task::none()
@@ -485,6 +511,18 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
new_options.par_sort_by_key(|x| x.display_name.len());
tile.options = AppIndex::from_apps(new_options);
+ let mut shell_map = HashMap::new();
+
+ for shell in &tile.config.shells {
+ if let Some(has_hk) = &shell.hotkey
+ && let Some(hotkey) = Shortcut::parse(has_hk).ok()
+ {
+ shell_map.insert(hotkey, shell.clone());
+ }
+ }
+
+ tile.hotkeys.shells = shell_map;
+
Task::none()
}
diff --git a/src/main.rs b/src/main.rs
index 6b06a7c4..82c88636 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,18 +12,14 @@ mod styles;
mod unit_conversion;
mod utils;
-use std::{fs::OpenOptions, path::Path};
+use std::{collections::HashMap, fs::OpenOptions, path::Path};
use crate::{
- app::tile::{self, Tile},
+ app::tile::{self, Hotkeys, Tile},
config::Config,
- platform::macos::get_autostart_status,
+ platform::macos::{get_autostart_status, launching::Shortcut},
};
-use global_hotkey::{
- GlobalHotKeyManager,
- hotkey::{Code, HotKey, Modifiers},
-};
use log::info;
use tracing_subscriber::{EnvFilter, Layer, util::SubscriberInitExt};
@@ -67,36 +63,33 @@ fn main() -> iced::Result {
info!("Config loaded");
- let manager = GlobalHotKeyManager::new().unwrap();
+ let show_hide =
+ Shortcut::parse(&config.toggle_hotkey).unwrap_or(Shortcut::parse("option+space").unwrap());
- let show_hide = config
- .toggle_hotkey
- .parse()
- .unwrap_or(HotKey::new(Some(Modifiers::ALT), Code::Space));
+ let cbhist = Shortcut::parse(&config.clipboard_hotkey.to_lowercase())
+ .unwrap_or_else(|_| Shortcut::parse("cmd+shift+c").unwrap());
- let cbhist = config
- .clipboard_hotkey
- .parse()
- .unwrap_or("SUPER+SHIFT+C".parse().unwrap());
+ let mut shell_map = HashMap::new();
- let mut hotkeys = vec![show_hide, cbhist];
for shell in &config.shells {
if let Some(hk_str) = &shell.hotkey
- && let Ok(hk) = hk_str.parse::()
+ && let Ok(hk) = Shortcut::parse(hk_str)
{
- hotkeys.push(hk);
+ shell_map.insert(hk, shell.clone());
}
}
- manager
- .register_all(&hotkeys)
- .expect("Unable to register hotkeys");
+ let hotkeys = Hotkeys {
+ toggle: show_hide,
+ clipboard_hotkey: cbhist,
+ shells: shell_map,
+ };
info!("Hotkeys loaded");
info!("Starting rustcast");
iced::daemon(
- move || tile::elm::new(show_hide, &config),
+ move || tile::elm::new(hotkeys.clone(), &config),
tile::update::handle_update,
tile::elm::view,
)
diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs
new file mode 100644
index 00000000..5ef49b4a
--- /dev/null
+++ b/src/platform/macos/launching.rs
@@ -0,0 +1,234 @@
+use std::sync::{Arc, Mutex};
+
+use block2::RcBlock;
+use objc2_app_kit::{NSEvent, NSEventMask, NSEventModifierFlags, NSEventType};
+
+use crate::app::{Message, tile::ExtSender};
+
+pub fn global_handler(sender: ExtSender) {
+ local_handler(sender.clone());
+ let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged;
+ let sender = Arc::new(Mutex::new(sender.0.clone()));
+
+ let block = RcBlock::new({
+ move |event: std::ptr::NonNull| {
+ let event = unsafe { event.as_ref() };
+ let event_type = event.r#type();
+
+ let key_code = event.keyCode();
+ let mods = event.modifierFlags()
+ & (NSEventModifierFlags::Command
+ | NSEventModifierFlags::Option
+ | NSEventModifierFlags::Control
+ | NSEventModifierFlags::Function
+ | NSEventModifierFlags::CapsLock
+ | NSEventModifierFlags::Shift);
+
+ let shortcut = match event_type {
+ NSEventType::KeyDown => Shortcut {
+ key_code: Some(key_code),
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
+ },
+ NSEventType::FlagsChanged => Shortcut {
+ key_code: None,
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
+ },
+ _ => return,
+ };
+
+ let mut s = sender.lock().unwrap();
+ let _ = s.try_send(Message::KeyPressed(shortcut));
+ }
+ });
+
+ NSEvent::addGlobalMonitorForEventsMatchingMask_handler(mask, &block);
+}
+
+pub fn local_handler(sender: ExtSender) {
+ let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged;
+ let sender = Arc::new(Mutex::new(sender.0.clone()));
+
+ let block = RcBlock::new({
+ move |event: std::ptr::NonNull| -> *mut NSEvent {
+ let event_ref = unsafe { event.as_ref() };
+ let event_type = event_ref.r#type();
+
+ let key_code = event_ref.keyCode();
+ let mods = event_ref.modifierFlags()
+ & (NSEventModifierFlags::Command
+ | NSEventModifierFlags::Option
+ | NSEventModifierFlags::Control
+ | NSEventModifierFlags::Function
+ | NSEventModifierFlags::CapsLock
+ | NSEventModifierFlags::Shift);
+
+ let shortcut = match event_type {
+ NSEventType::KeyDown => Shortcut {
+ key_code: Some(key_code),
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
+ },
+ NSEventType::FlagsChanged => Shortcut {
+ key_code: None,
+ mods: if mods.0 != 0 { Some(mods.0) } else { None },
+ },
+ _ => return event.as_ptr(), // pass through unhandled events
+ };
+
+ let mut s = sender.lock().unwrap();
+ let _ = s.try_send(Message::KeyPressed(shortcut));
+
+ event.as_ptr()
+ }
+ });
+
+ unsafe {
+ NSEvent::addLocalMonitorForEventsMatchingMask_handler(mask, &block);
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Shortcut {
+ pub key_code: Option,
+ pub mods: Option,
+}
+
+impl Shortcut {
+ pub fn new(key_code: Option, mods: Option) -> Self {
+ Self { key_code, mods }
+ }
+
+ pub fn parse(s: &str) -> Result {
+ let parts: Vec<&str> = s.split('+').map(|p| p.trim()).collect();
+
+ let mut mods: usize = 0;
+ let mut key_code: Option = None;
+ let mut has_mods = false;
+
+ for part in &parts {
+ match part.to_lowercase().as_str() {
+ "cmd" | "command" | "super" => {
+ mods |= NSEventModifierFlags::Command.0;
+ has_mods = true;
+ }
+ "opt" | "option" | "alt" => {
+ mods |= NSEventModifierFlags::Option.0;
+ has_mods = true;
+ }
+ "capslock" | "caps" | "caps lock" => mods |= NSEventModifierFlags::CapsLock.0,
+ "ctrl" | "control" => {
+ mods |= NSEventModifierFlags::Control.0;
+ has_mods = true;
+ }
+ "shift" => {
+ mods |= NSEventModifierFlags::Shift.0;
+ has_mods = true;
+ }
+ "fn" | "function" => {
+ mods |= NSEventModifierFlags::Function.0;
+ has_mods = true;
+ }
+ key => {
+ if key_code.is_some() {
+ return Err(format!("Multiple keys specified: '{}'", s));
+ }
+ key_code = Some(str_to_keycode(key)?);
+ }
+ }
+ }
+
+ Ok(Shortcut::new(
+ key_code,
+ if has_mods { Some(mods) } else { None },
+ ))
+ }
+}
+
+fn str_to_keycode(s: &str) -> Result {
+ let code = match s.to_lowercase().as_str() {
+ // Letters
+ "a" => 0x00,
+ "s" => 0x01,
+ "d" => 0x02,
+ "f" => 0x03,
+ "h" => 0x04,
+ "g" => 0x05,
+ "z" => 0x06,
+ "x" => 0x07,
+ "c" => 0x08,
+ "v" => 0x09,
+ "b" => 0x0b,
+ "q" => 0x0c,
+ "w" => 0x0d,
+ "e" => 0x0e,
+ "r" => 0x0f,
+ "y" => 0x10,
+ "t" => 0x11,
+ "o" => 0x1f,
+ "u" => 0x20,
+ "i" => 0x22,
+ "p" => 0x23,
+ "l" => 0x25,
+ "j" => 0x26,
+ "k" => 0x28,
+ "n" => 0x2d,
+ "m" => 0x2e,
+
+ // Numbers
+ "1" => 0x12,
+ "2" => 0x13,
+ "3" => 0x14,
+ "4" => 0x15,
+ "5" => 0x17,
+ "6" => 0x16,
+ "7" => 0x1a,
+ "8" => 0x1c,
+ "9" => 0x19,
+ "0" => 0x1d,
+
+ // Special keys
+ "return" | "enter" => 0x24,
+ "tab" => 0x30,
+ "space" => 0x31,
+ "delete" | "backspace" => 0x33,
+ "escape" | "esc" => 0x35,
+ "left" | "arrowleft" => 0x7b,
+ "right" | "arrowright" => 0x7c,
+ "down" | "arrowdown" => 0x7d,
+ "up" | "arrowup" => 0x7e,
+ "home" => 0x73,
+ "end" => 0x77,
+ "pageup" => 0x74,
+ "pagedown" => 0x79,
+
+ // Function keys
+ "f1" => 0x7a,
+ "f2" => 0x78,
+ "f3" => 0x63,
+ "f4" => 0x76,
+ "f5" => 0x60,
+ "f6" => 0x61,
+ "f7" => 0x62,
+ "f8" => 0x64,
+ "f9" => 0x65,
+ "f10" => 0x6d,
+ "f11" => 0x67,
+ "f12" => 0x6f,
+
+ // Symbols
+ "-" | "minus" => 0x1b,
+ "=" | "equal" => 0x18,
+ "[" | "bracketleft" => 0x21,
+ "]" | "bracketright" => 0x1e,
+ "\\" | "backslash" => 0x2a,
+ ";" | "semicolon" => 0x29,
+ "'" | "quote" => 0x27,
+ "`" | "backquote" | "grave" => 0x32,
+ "," | "comma" => 0x2b,
+ "." | "period" => 0x2f,
+ "/" | "slash" => 0x2c,
+
+ _ => return Err(format!("Unknown key: '{}'", s)),
+ };
+
+ Ok(code)
+}
diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs
index ab7c05c0..eeab1289 100644
--- a/src/platform/macos/mod.rs
+++ b/src/platform/macos/mod.rs
@@ -1,6 +1,7 @@
//! Macos specific logic, such as window settings, etc.
pub mod discovery;
pub mod haptics;
+pub mod launching;
use iced::wgpu::rwh::WindowHandle;
From f07a5d5fc473bc93dab7e6bb58363b3a60f57051 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Thu, 2 Apr 2026 15:36:01 +0800
Subject: [PATCH 33/51] remove unused imports
---
src/app/menubar.rs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/app/menubar.rs b/src/app/menubar.rs
index 2d1c87a0..7b82e887 100644
--- a/src/app/menubar.rs
+++ b/src/app/menubar.rs
@@ -2,7 +2,6 @@
use std::{collections::HashMap, io::Cursor};
-use global_hotkey::hotkey::{Code, Modifiers};
use image::{DynamicImage, ImageReader};
use log::info;
use tray_icon::{
From ea00fe31991a87c38d6fec85c341a55c5f1e3ac9 Mon Sep 17 00:00:00 2001
From: Secretised <132474703+unsecretised@users.noreply.github.com>
Date: Sat, 11 Apr 2026 16:30:40 +0800
Subject: [PATCH 34/51] Update issue templates
---
.github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++++++++++++++++
.github/ISSUE_TEMPLATE/feature_request.md | 21 +++++++++++++++
2 files changed, 53 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..5537e972
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Unexpected behaviour / crashes
+title: "[bug]"
+labels: bug, not assigned
+assignees: ''
+type: Bug
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Which macos version? (please complete the following information):**
+ - OS: [e.g. MacOS Sequoia 15.5]
+ - Rustcast Version [e.g. v0.7.3]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..c7a35a7f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,21 @@
+---
+name: Feature request
+about: Request a feature into Rustcast
+title: "[feat]"
+labels: enhancement, not assigned
+assignees: ''
+type: Feature
+
+---
+
+**Why do you think this is a good feature?**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**What have you been using currently to solve this?**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
From f4b1c1ad73b4623984f2f8839c827d1688b8e1e2 Mon Sep 17 00:00:00 2001
From: Ansel <238927804+asong56@users.noreply.github.com>
Date: Sun, 12 Apr 2026 21:29:35 -0400
Subject: [PATCH 35/51] Enhance search_prefix to include prefix with hyphen
---
src/app/tile.rs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/app/tile.rs b/src/app/tile.rs
index 05c41668..0ccbb4e2 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -54,7 +54,10 @@ 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 ParallelIterator
- + 'a {
self.by_name.par_iter().filter_map(move |(name, app)| {
- if name.starts_with(prefix) || name.contains(format!(" {prefix}").as_str()) {
+ if name.starts_with(prefix)
+ || name.contains(format!(" {prefix}").as_str())
+ || name.contains(format!("-{prefix}").as_str())
+ {
Some(app)
} else {
None
From a5f2c76f8190ce12cd2c12ec371e5f762027512e Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sun, 19 Apr 2026 18:53:59 +0800
Subject: [PATCH 36/51] Update website
---
docs/index.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/index.html b/docs/index.html
index 1f17b1d8..63dcd3ea 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -75,8 +75,8 @@
The fastest launcher
you'll ever use
-
500+ stars
-
1.2k+ downloads
+
550+ stars
+
1.9k+ downloads
100% free forever
MIT license
From d1420a2be2ac8072a190537365fa80511347a521 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 12:34:38 +0800
Subject: [PATCH 37/51] add deeplinks support
---
Cargo.lock | 1 +
Cargo.toml | 5 +
assets/macos/RustCast.app/Contents/Info.plist | 67 ++++---
src/app.rs | 1 +
src/app/tile.rs | 1 +
src/app/tile/elm.rs | 2 +
src/app/tile/update.rs | 30 +++
src/platform/macos/mod.rs | 1 +
src/platform/macos/urlscheme.rs | 186 ++++++++++++++++++
9 files changed, 266 insertions(+), 28 deletions(-)
create mode 100644 src/platform/macos/urlscheme.rs
diff --git a/Cargo.lock b/Cargo.lock
index c059a04c..59d8cc85 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3855,6 +3855,7 @@ version = "0.7.2"
dependencies = [
"arboard",
"block2 0.6.2",
+ "crossbeam-channel",
"emojis",
"global-hotkey",
"iced",
diff --git a/Cargo.toml b/Cargo.toml
index 812a7cbb..d69ba843 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/RustCastLabs/rustcast"
[dependencies]
arboard = "3.6.1"
block2 = "0.6.2"
+crossbeam-channel = "0.5.15"
emojis = "0.8.0"
global-hotkey = "0.7.0"
iced = { version = "0.14.0", features = ["image", "tokio"] }
@@ -35,3 +36,7 @@ toml = "0.9.8"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tray-icon = "0.21.3"
url = { version = "2.5.8", default-features = false }
+
+[package.metadata.bundle]
+info_plist = "assets/macos/RustCast.app/Contents/Resources/Info.plist"
+identifier = "com.umangsurana.rustcast"
diff --git a/assets/macos/RustCast.app/Contents/Info.plist b/assets/macos/RustCast.app/Contents/Info.plist
index 298774ff..9f50001f 100644
--- a/assets/macos/RustCast.app/Contents/Info.plist
+++ b/assets/macos/RustCast.app/Contents/Info.plist
@@ -1,32 +1,43 @@
-
- CFBundleDevelopmentRegion
- en
- CFBundleExecutable
- rustcast
- CFBundleIdentifier
- com.umangsurana.rustcast
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- RustCast
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- 1
- LSUIElement
-
- NSHighResolutionCapable
-
- NSHumanReadableCopyright
- Copyright © 2025 Umang Surana. All rights reserved.
- NSInputMonitoringUsageDescription
- RustCast needs to monitor keyboard input to detect global shortcuts and control casting.
- CFBundleIconFile
- icon
-
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ rustcast
+ CFBundleIdentifier
+ com.umangsurana.rustcast
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ RustCast
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSUIElement
+
+ NSHighResolutionCapable
+
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ rustcast
+
+ CFBundleURLName
+ com.umangsurana.rustcast
+
+
+ NSHumanReadableCopyright
+ Copyright © 2025 Umang Surana. All rights reserved.
+ NSInputMonitoringUsageDescription
+ RustCast needs to monitor keyboard input to detect global shortcuts and control casting.
+ CFBundleIconFile
+ icon
+
diff --git a/src/app.rs b/src/app.rs
index 68b19e41..6892bdb5 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -80,6 +80,7 @@ pub enum Editable {
/// The message type that iced uses for actions that can do something
#[derive(Debug, Clone)]
pub enum Message {
+ UriReceived(String),
WriteConfig(bool),
SaveRanking,
ToggleAutoStartup(bool),
diff --git a/src/app/tile.rs b/src/app/tile.rs
index 0ccbb4e2..7b131bee 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -231,6 +231,7 @@ impl Tile {
Subscription::batch([
Subscription::run(handle_hot_reloading),
keyboard,
+ Subscription::run(crate::platform::macos::urlscheme::url_stream),
Subscription::run(handle_recipient),
Subscription::run(handle_version_and_rankings),
Subscription::run(handle_clipboard_history),
diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs
index 87fadf9c..7fbe2ab9 100644
--- a/src/app/tile/elm.rs
+++ b/src/app/tile/elm.rs
@@ -66,6 +66,8 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) {
)
.unwrap_or(HashMap::new());
+ crate::platform::macos::urlscheme::install();
+
(
Tile {
update_available: false,
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index f3aa95b2..1684476d 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -15,6 +15,7 @@ use log::info;
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator;
use rayon::slice::ParallelSliceMut;
+use url::Url;
use crate::app::Editable;
use crate::app::SetConfigBufferFields;
@@ -46,6 +47,12 @@ use crate::{app::DEFAULT_WINDOW_HEIGHT, platform::perform_haptic};
use crate::{app::Move, platform::HapticPattern};
use crate::{app::RUSTCAST_DESC_NAME, platform::get_installed_apps};
+fn extract_target(url: &Url) -> Option {
+ url.query_pairs()
+ .find(|(key, _)| key == "target")
+ .map(|(_, value)| value.into_owned())
+}
+
/// Handle the "elm" update
pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
match message {
@@ -64,6 +71,29 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
}
}
+ Message::UriReceived(uri) => {
+ let Ok(url) = Url::parse(&uri) else {
+ return Task::none();
+ };
+
+ match url.host_str().unwrap_or("") {
+ "open" => extract_target(&url)
+ .and_then(|x| tile.options.by_name.get(&x).map(|x| x.to_owned()))
+ .map(|app| match app.open_command {
+ AppCommand::Function(a) => Task::done(Message::RunFunction(a)),
+ AppCommand::Display => Task::none(),
+ AppCommand::Message(msg) => Task::done(msg),
+ })
+ .unwrap_or(Task::none()),
+
+ "show" => open_window(DEFAULT_WINDOW_HEIGHT),
+
+ "quit" => Task::done(Message::RunFunction(Function::Quit)),
+
+ _ => Task::none(),
+ }
+ }
+
Message::UpdateAvailable => {
tile.update_available = true;
Task::done(Message::ReloadConfig)
diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs
index eeab1289..0441f05d 100644
--- a/src/platform/macos/mod.rs
+++ b/src/platform/macos/mod.rs
@@ -2,6 +2,7 @@
pub mod discovery;
pub mod haptics;
pub mod launching;
+pub mod urlscheme;
use iced::wgpu::rwh::WindowHandle;
diff --git a/src/platform/macos/urlscheme.rs b/src/platform/macos/urlscheme.rs
new file mode 100644
index 00000000..abf08737
--- /dev/null
+++ b/src/platform/macos/urlscheme.rs
@@ -0,0 +1,186 @@
+//! macOS URL scheme handler for `nexus://` deep links
+//!
+//! On macOS, clicking a `nexus://` link delivers the URL via Apple Events
+//! (`kInternetEventClass` / `kAEGetURL`), not as a command-line argument.
+//!
+//! This module registers a handler with `NSAppleEventManager` to receive
+//! those events and forwards URLs through a crossbeam channel consumed by
+//! an async stream subscription.
+//!
+//! **Why NSAppleEventManager instead of NSApplicationDelegate?**
+//! Iced/winit owns the `NSApplication` delegate for window and input event
+//! handling. Replacing it with our own delegate breaks the entire event
+//! chain and causes crashes. `NSAppleEventManager` hooks into URL delivery
+//! at a lower level without touching the delegate.
+
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::time::Duration;
+
+use crossbeam_channel::{Receiver, Sender};
+use objc2::rc::Retained;
+use objc2::runtime::AnyObject;
+use objc2::{MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
+use objc2_foundation::{NSObject, NSObjectProtocol};
+use once_cell::sync::Lazy;
+
+use crate::app::Message;
+
+/// Channel for forwarding URLs from the Apple Event handler to the Iced event loop.
+///
+/// `crossbeam_channel` is used because both `Sender` and `Receiver` are
+/// `Send + Sync`, which is required for use in a `static`. The standard
+/// library's `mpsc::Receiver` is not `Sync` and would fail to compile.
+static URL_CHANNEL: Lazy<(Sender, Receiver)> =
+ Lazy::new(crossbeam_channel::unbounded);
+
+/// Flag set during app shutdown so the `spawn_blocking` recv loop can exit.
+static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
+
+/// Apple Event FourCharCode for `kInternetEventClass` and `kAEGetURL` (both `'GURL'`).
+const K_AE_GET_URL: u32 = u32::from_be_bytes(*b"GURL");
+
+/// Apple Event FourCharCode for `keyDirectObject` (`'----'`), the parameter
+/// key that contains the URL string in a "get URL" event.
+const KEY_DIRECT_OBJECT: u32 = u32::from_be_bytes(*b"----");
+
+define_class!(
+ #[unsafe(super(NSObject))]
+ #[thread_kind = MainThreadOnly]
+ #[name = "NexusURLHandler"]
+ struct UrlHandler;
+
+ unsafe impl NSObjectProtocol for UrlHandler {}
+
+ /// Handler method registered with `NSAppleEventManager`. The selector
+ /// `handleGetURLEvent:withReplyEvent:` matches what AppKit expects for
+ /// Apple Event callbacks.
+ impl UrlHandler {
+ #[unsafe(method(handleGetURLEvent:withReplyEvent:))]
+ fn handle_get_url_event(&self, event: &AnyObject, _reply: &AnyObject) {
+ // event is an NSAppleEventDescriptor. Extract the direct object
+ // parameter which contains the URL as an NSAppleEventDescriptor,
+ // then get its stringValue (an NSString).
+ let descriptor: *mut AnyObject =
+ unsafe { msg_send![event, paramDescriptorForKeyword: KEY_DIRECT_OBJECT] };
+ if descriptor.is_null() {
+ return;
+ }
+ let ns_string: *mut AnyObject = unsafe { msg_send![&*descriptor, stringValue] };
+ if ns_string.is_null() {
+ return;
+ }
+ let utf8: *const std::ffi::c_char = unsafe { msg_send![&*ns_string, UTF8String] };
+ if utf8.is_null() {
+ return;
+ }
+ let url_str = unsafe { std::ffi::CStr::from_ptr(utf8) }
+ .to_string_lossy()
+ .to_string();
+ if url_str.to_lowercase().starts_with("rustcast://") {
+ let _ = URL_CHANNEL.0.send(url_str);
+ }
+ }
+ }
+);
+
+impl UrlHandler {
+ fn new(mtm: MainThreadMarker) -> Retained {
+ // SAFETY: `Self::alloc(mtm)` returns a valid allocated instance of our
+ // NSObject subclass. Sending `init` to a freshly allocated NSObject
+ // subclass with no custom ivars is the standard Objective-C
+ // initialisation pattern and always succeeds.
+ unsafe { msg_send![Self::alloc(mtm), init] }
+ }
+}
+
+/// Install the macOS URL scheme handler via `NSAppleEventManager`.
+///
+/// Must be called **after** the Iced/winit event loop has been created
+/// (i.e. from `NexusApp::new()`), so that AppKit is fully initialized.
+///
+/// Registers for `kInternetEventClass` / `kAEGetURL` events, which macOS
+/// sends when a `nexus://` URL is opened (clicked in browser, Finder, etc.).
+pub fn install() {
+ let Some(mtm) = MainThreadMarker::new() else {
+ eprintln!("macos_url: not on main thread, skipping URL handler install");
+ return;
+ };
+
+ let handler = UrlHandler::new(mtm);
+
+ // Get [NSAppleEventManager sharedAppleEventManager]
+ let mgr: *mut AnyObject = unsafe {
+ msg_send![
+ objc2::runtime::AnyClass::get(c"NSAppleEventManager")
+ .expect("NSAppleEventManager class not found"),
+ sharedAppleEventManager
+ ]
+ };
+ assert!(
+ !mgr.is_null(),
+ "macos_url: sharedAppleEventManager returned nil"
+ );
+
+ // Register: [mgr setEventHandler:handler
+ // andSelector:@selector(handleGetURLEvent:withReplyEvent:)
+ // forEventClass:kInternetEventClass
+ // andEventID:kAEGetURL]
+ let handler_sel = sel!(handleGetURLEvent:withReplyEvent:);
+ unsafe {
+ let _: () = msg_send![
+ &*mgr,
+ setEventHandler: &*handler,
+ andSelector: handler_sel,
+ forEventClass: K_AE_GET_URL,
+ andEventID: K_AE_GET_URL
+ ];
+ }
+
+ // Leak the handler so it lives for the entire process.
+ //
+ // `UrlHandler` is `MainThreadOnly` (`!Send + !Sync`), so
+ // `Retained` cannot be stored in a `static`. Leaking
+ // is the standard pattern for process-lifetime Objective-C objects.
+ //
+ // Unlike `NSApplication.delegate` (which is weak), the Apple Event
+ // Manager retains a strong reference — but leaking is still correct
+ // because we never want to unregister the handler.
+ std::mem::forget(handler);
+}
+
+/// Signal the URL stream to stop so the `spawn_blocking` task can exit
+/// and tokio's runtime drop won't hang.
+///
+/// Must be called before `iced::window::close()` on macOS.
+#[allow(unused)]
+pub fn shutdown() {
+ SHUTTING_DOWN.store(true, Ordering::Relaxed);
+}
+
+/// Async stream that yields URLs received via Apple Events.
+///
+/// Uses `recv_timeout` inside `spawn_blocking` so the blocking thread
+/// wakes periodically and can exit when the tokio runtime shuts down
+/// (e.g., on app quit). Without this, `recv()` blocks indefinitely and
+/// causes a hang on macOS during quit.
+pub fn url_stream() -> impl iced::futures::Stream- {
+ iced::futures::stream::unfold((), |()| async {
+ let url = tokio::task::spawn_blocking(|| {
+ loop {
+ if SHUTTING_DOWN.load(Ordering::Relaxed) {
+ return None;
+ }
+ match URL_CHANNEL.1.recv_timeout(Duration::from_millis(500)) {
+ Ok(url) => return Some(url),
+ Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
+ Err(crossbeam_channel::RecvTimeoutError::Disconnected) => return None,
+ }
+ }
+ })
+ .await
+ .ok()
+ .flatten()?;
+
+ Some((Message::UriReceived(url), ()))
+ })
+}
From e7ebdea8d43ac77b186c4f9c2d6e497c7e878ad3 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 12:43:12 +0800
Subject: [PATCH 38/51] remove cargo bundle stuff
---
Cargo.toml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index d69ba843..7cb8af64 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,7 +36,3 @@ toml = "0.9.8"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tray-icon = "0.21.3"
url = { version = "2.5.8", default-features = false }
-
-[package.metadata.bundle]
-info_plist = "assets/macos/RustCast.app/Contents/Resources/Info.plist"
-identifier = "com.umangsurana.rustcast"
From c53aacf37f8b8afdae03d298d1d6b44a6f0fb33a Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 12:43:49 +0800
Subject: [PATCH 39/51] Fix attempt
---
assets/entitlements.plist | 18 ++++++++++++++++++
scripts/sign-macos.sh | 3 +++
2 files changed, 21 insertions(+)
create mode 100644 assets/entitlements.plist
diff --git a/assets/entitlements.plist b/assets/entitlements.plist
new file mode 100644
index 00000000..a56b2511
--- /dev/null
+++ b/assets/entitlements.plist
@@ -0,0 +1,18 @@
+
+
+
+
+
+ com.apple.security.temporary-exception.accessibility
+
+
+
+ com.apple.security.app-sandbox
+
+
+
+ com.apple.security.device.input-monitoring
+
+
+
diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh
index 881f50fc..6b99c8e2 100755
--- a/scripts/sign-macos.sh
+++ b/scripts/sign-macos.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env -S bash -e
+ENTITLEMENTS_PATH="assets/macos/entitlements.plist"
+
APP_BUNDLE_PATH="${APP_BUNDLE_PATH:?APP_BUNDLE_PATH not set}"
# 1. Create a temporary keychain and import certificate
@@ -29,6 +31,7 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: \
# 2. Sign app bundle
codesign --deep --force --options runtime --timestamp \
+ --entitlements $ENTITLEMENTS_PATH \
--sign "$MACOS_CERTIFICATE_NAME" \
"$APP_BUNDLE_PATH"
From b8f006fc0cc7959773d5185be9fc5828b19b10c5 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 13:09:04 +0800
Subject: [PATCH 40/51] update path
---
assets/entitlements.plist | 9 ++-------
scripts/sign-macos.sh | 2 +-
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/assets/entitlements.plist b/assets/entitlements.plist
index a56b2511..cf673fc9 100644
--- a/assets/entitlements.plist
+++ b/assets/entitlements.plist
@@ -3,16 +3,11 @@
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-
- com.apple.security.temporary-exception.accessibility
-
-
-
com.apple.security.app-sandbox
-
-
com.apple.security.device.input-monitoring
+ com.apple.security.temporary-exception.accessibility
+
diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh
index 6b99c8e2..3b1c1620 100755
--- a/scripts/sign-macos.sh
+++ b/scripts/sign-macos.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env -S bash -e
-ENTITLEMENTS_PATH="assets/macos/entitlements.plist"
+ENTITLEMENTS_PATH="assets/entitlements.plist"
APP_BUNDLE_PATH="${APP_BUNDLE_PATH:?APP_BUNDLE_PATH not set}"
From b182c9e9b1ec7ac2812edd979327ca4af75fe5f4 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 13:17:55 +0800
Subject: [PATCH 41/51] ensure accessibility
---
src/platform/macos/accessibility.rs | 89 +++++++++++++++++++++++++++++
src/platform/macos/launching.rs | 3 +-
src/platform/macos/login.rs | 0
src/platform/macos/mod.rs | 1 +
4 files changed, 92 insertions(+), 1 deletion(-)
create mode 100644 src/platform/macos/accessibility.rs
delete mode 100644 src/platform/macos/login.rs
diff --git a/src/platform/macos/accessibility.rs b/src/platform/macos/accessibility.rs
new file mode 100644
index 00000000..f994f843
--- /dev/null
+++ b/src/platform/macos/accessibility.rs
@@ -0,0 +1,89 @@
+use std::ffi::c_void;
+use std::thread;
+use std::time::{Duration, Instant};
+
+use objc2::rc::autoreleasepool;
+use objc2::runtime::AnyObject;
+use objc2::{class, msg_send};
+use log::info;
+
+#[link(name = "ApplicationServices", kind = "framework")]
+unsafe extern "C" {
+ fn AXIsProcessTrustedWithOptions(options: *const c_void) -> bool;
+
+ static kAXTrustedCheckOptionPrompt: *const c_void;
+}
+
+#[link(name = "CoreFoundation", kind = "framework")]
+unsafe extern "C" {
+ static kCFBooleanTrue: *const c_void;
+ static kCFBooleanFalse: *const c_void;
+}
+
+const AX_POLL_INTERVAL: Duration = Duration::from_millis(250);
+const AX_POLL_TIMEOUT: Duration = Duration::from_secs(30);
+
+#[inline]
+fn ax_is_trusted() -> bool {
+ unsafe {
+ autoreleasepool(|_| {
+ let keys: [*mut AnyObject; 1] = [kAXTrustedCheckOptionPrompt as *mut AnyObject];
+ let vals: [*mut AnyObject; 1] = [kCFBooleanFalse as *mut AnyObject];
+ let dict: *mut AnyObject = msg_send![
+ class!(NSDictionary),
+ dictionaryWithObjects: vals.as_ptr(),
+ forKeys: keys.as_ptr(),
+ count: 1usize
+ ];
+
+ AXIsProcessTrustedWithOptions(dict.cast())
+ })
+ }
+}
+
+#[allow(unsafe_op_in_unsafe_fn)]
+unsafe fn prompt_ax_trust_dialog() {
+ autoreleasepool(|_| {
+ let keys: [*mut AnyObject; 1] = [kAXTrustedCheckOptionPrompt as *mut AnyObject];
+ let vals: [*mut AnyObject; 1] = [kCFBooleanTrue as *mut AnyObject];
+
+ let dict: *mut AnyObject = msg_send![
+ class!(NSDictionary),
+ dictionaryWithObjects: vals.as_ptr(),
+ forKeys: keys.as_ptr(),
+ count: 1usize
+ ];
+
+ let _ = AXIsProcessTrustedWithOptions(dict.cast());
+ });
+}
+
+pub fn ensure_accessibility_permission() {
+ if ax_is_trusted() {
+ return;
+ }
+
+ info!("Accessibility permission is not granted; prompting user for permission now.");
+
+ unsafe { prompt_ax_trust_dialog() };
+
+ let start = Instant::now();
+ loop {
+ if ax_is_trusted() {
+ info!("Accessibility permission granted");
+ return;
+ }
+
+ if start.elapsed() >= AX_POLL_TIMEOUT {
+ break;
+ }
+
+ thread::sleep(AX_POLL_INTERVAL);
+ }
+
+ println!(
+ "Rift still does not have accessibility permission. Enable it in System Settings > Privacy & Security > Accessibility, then restart Rift."
+ );
+
+ std::process::exit(1);
+}
diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs
index 5ef49b4a..087bb026 100644
--- a/src/platform/macos/launching.rs
+++ b/src/platform/macos/launching.rs
@@ -3,9 +3,10 @@ use std::sync::{Arc, Mutex};
use block2::RcBlock;
use objc2_app_kit::{NSEvent, NSEventMask, NSEventModifierFlags, NSEventType};
-use crate::app::{Message, tile::ExtSender};
+use crate::{app::{Message, tile::ExtSender}, platform::macos::accessibility::ensure_accessibility_permission};
pub fn global_handler(sender: ExtSender) {
+ ensure_accessibility_permission();
local_handler(sender.clone());
let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged;
let sender = Arc::new(Mutex::new(sender.0.clone()));
diff --git a/src/platform/macos/login.rs b/src/platform/macos/login.rs
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs
index eeab1289..39cb9c41 100644
--- a/src/platform/macos/mod.rs
+++ b/src/platform/macos/mod.rs
@@ -1,4 +1,5 @@
//! Macos specific logic, such as window settings, etc.
+pub mod accessibility;
pub mod discovery;
pub mod haptics;
pub mod launching;
From 4a6cbf700bda635ae2b32a6f4b2ecfc2507251d3 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 13:18:04 +0800
Subject: [PATCH 42/51] format -_-
---
src/platform/macos/accessibility.rs | 2 +-
src/platform/macos/launching.rs | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/platform/macos/accessibility.rs b/src/platform/macos/accessibility.rs
index f994f843..2617a412 100644
--- a/src/platform/macos/accessibility.rs
+++ b/src/platform/macos/accessibility.rs
@@ -2,10 +2,10 @@ use std::ffi::c_void;
use std::thread;
use std::time::{Duration, Instant};
+use log::info;
use objc2::rc::autoreleasepool;
use objc2::runtime::AnyObject;
use objc2::{class, msg_send};
-use log::info;
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs
index 087bb026..e6d22a98 100644
--- a/src/platform/macos/launching.rs
+++ b/src/platform/macos/launching.rs
@@ -3,7 +3,10 @@ use std::sync::{Arc, Mutex};
use block2::RcBlock;
use objc2_app_kit::{NSEvent, NSEventMask, NSEventModifierFlags, NSEventType};
-use crate::{app::{Message, tile::ExtSender}, platform::macos::accessibility::ensure_accessibility_permission};
+use crate::{
+ app::{Message, tile::ExtSender},
+ platform::macos::accessibility::ensure_accessibility_permission,
+};
pub fn global_handler(sender: ExtSender) {
ensure_accessibility_permission();
From 04927ded43a1b944ca20d1578c73335250b43292 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 13:18:41 +0800
Subject: [PATCH 43/51] change note and add credits
---
src/platform/macos/accessibility.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/platform/macos/accessibility.rs b/src/platform/macos/accessibility.rs
index 2617a412..38b4b73e 100644
--- a/src/platform/macos/accessibility.rs
+++ b/src/platform/macos/accessibility.rs
@@ -1,3 +1,4 @@
+/// Taken from:
use std::ffi::c_void;
use std::thread;
use std::time::{Duration, Instant};
@@ -82,7 +83,7 @@ pub fn ensure_accessibility_permission() {
}
println!(
- "Rift still does not have accessibility permission. Enable it in System Settings > Privacy & Security > Accessibility, then restart Rift."
+ "Rustcast still does not have accessibility permission. Enable it in System Settings > Privacy & Security > Accessibility, then restart Rustcast."
);
std::process::exit(1);
From 04ef73aabe386810ffc219c7cf317baf592bb623 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 14:16:19 +0800
Subject: [PATCH 44/51] format + deeplinks
---
.github/ISSUE_TEMPLATE/bug_report.md | 23 +++----
.github/ISSUE_TEMPLATE/feature_request.md | 17 +++--
docs/index.html | 77 +++++++++++++++++++++++
3 files changed, 95 insertions(+), 22 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5537e972..e3968f0d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -5,28 +5,25 @@ title: "[bug]"
labels: bug, not assigned
assignees: ''
type: Bug
-
---
-**Describe the bug**
-A clear and concise description of what the bug is.
+**Describe the bug** A clear and concise description of what the bug is.
+
+**To Reproduce** Steps to reproduce the behavior:
-**To Reproduce**
-Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-**Expected behavior**
-A clear and concise description of what you expected to happen.
+**Expected behavior** A clear and concise description of what you expected to
+happen.
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
+**Screenshots** If applicable, add screenshots to help explain your problem.
**Which macos version? (please complete the following information):**
- - OS: [e.g. MacOS Sequoia 15.5]
- - Rustcast Version [e.g. v0.7.3]
-**Additional context**
-Add any other context about the problem here.
+- OS: [e.g. MacOS Sequoia 15.5]
+- Rustcast Version [e.g. v0.7.3]
+
+**Additional context** Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index c7a35a7f..76d6f7a1 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -5,17 +5,16 @@ title: "[feat]"
labels: enhancement, not assigned
assignees: ''
type: Feature
-
---
-**Why do you think this is a good feature?**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+**Why do you think this is a good feature?** A clear and concise description of
+what the problem is. Ex. I'm always frustrated when [...]
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
+**Describe the solution you'd like** A clear and concise description of what you
+want to happen.
-**What have you been using currently to solve this?**
-A clear and concise description of any alternative solutions or features you've considered.
+**What have you been using currently to solve this?** A clear and concise
+description of any alternative solutions or features you've considered.
-**Additional context**
-Add any other context or screenshots about the feature request here.
+**Additional context** Add any other context or screenshots about the feature
+request here.
diff --git a/docs/index.html b/docs/index.html
index 63dcd3ea..f1b56293 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -260,6 +260,83 @@
Script your workflow.
Define your modes.
+
+
+
+
+
+
// deeplinks
+
Automate with
URL schemes.
+
+ RustCast exposes a rustcast:// URL scheme (v0.7.4+).
+ Trigger it from scripts, shortcuts, browsers, or any tool that can
+ open a URL.
+
+
+
+
+
open
+
Launch an App
+
+ Open any installed application by name.
+
+
+rustcast://open?target=obsidian
+
+
+
+
show
+
Show RustCast
+
+ Bring the launcher to the foreground — useful in Shortcuts or
+ shell scripts.
+
+
+rustcast://show
+
+
+
+
quit
+
Quit RustCast
+
+ Gracefully quit the app from a script or automation flow.
+
+
+rustcast://quit
+
+
+
+
+
+
+ Terminal — example usage
+ v0.7.4+
+
+
+
+ open "rustcast://show"
+
+
+ open "rustcast://open?target=obsidian"
+
+
+ open "rustcast://quit"
+
+
+
+
+
From 9b77e1aca9d2f0a5163c5794c2c7c34e2bffd8b7 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 22:52:31 +0800
Subject: [PATCH 45/51] attempt fix from orvar
---
scripts/sign-macos-broken.sh | 68 --------------------
scripts/sign-macos-old.sh | 39 ++++++++++++
scripts/sign-macos.sh | 118 ++++++++++++++++++++++++++---------
3 files changed, 126 insertions(+), 99 deletions(-)
delete mode 100755 scripts/sign-macos-broken.sh
create mode 100755 scripts/sign-macos-old.sh
diff --git a/scripts/sign-macos-broken.sh b/scripts/sign-macos-broken.sh
deleted file mode 100755
index eeafe4fa..00000000
--- a/scripts/sign-macos-broken.sh
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-
-RELEASE_DIR="target/release"
-APP_DIR="$RELEASE_DIR/macos"
-APP_NAME="Rustcast.app"
-APP_PATH="$APP_DIR/$APP_NAME"
-
-# --- Required env vars (using the names you provided) ---
-environment=(
- "MACOS_CERTIFICATE"
- "MACOS_CERTIFICATE_PWD"
- "MACOS_CI_KEYCHAIN_PWD"
- "MACOS_CERTIFICATE_NAME"
- "MACOS_NOTARIZATION_PWD"
- "MACOS_NOTARY_TEAM_ID"
- "MACOS_NOTARY_KEY_ID"
- "MACOS_NOTARY_KEY"
-)
-
-for var in "${environment[@]}"; do
- if [[ -z "${!var:-}" ]]; then
- echo "Error: $var is not set"
- exit 1
- fi
-done
-
-# Optional: only needed if you still want to keep this around
-: "${MACOS_NOTARISATION_APPLE_ID:=}"
-
-echo "Decoding certificate"
-echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
-
-echo "Installing cert in a new keychain"
-security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
-security default-keychain -s build.keychain
-security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
-security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
-security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
-
-echo "Signing..."
-/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime --timestamp "$APP_PATH" -v
-
-echo "Creating temp notarization archive"
-ditto -c -k --keepParent "$APP_PATH" "notarization.zip"
-
-echo "Notarize app (API key auth)"
-# MACOS_NOTARY_KEY can be either:
-# - the *contents* of the .p8 key, or
-# - base64 of the .p8 key (recommended for CI)
-#
-# If it's base64, decode it first.
-NOTARY_KEY_FILE="AuthKey.p8"
-if printf '%s' "$MACOS_NOTARY_KEY" | grep -q "BEGIN PRIVATE KEY"; then
- printf '%s' "$MACOS_NOTARY_KEY" > "$NOTARY_KEY_FILE"
-else
- printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$NOTARY_KEY_FILE"
-fi
-
-# xcrun notarytool submit "notarization.zip" \
-# --team-id "$MACOS_NOTARY_TEAM_ID" \
-# --issuer "$MACOS_NOTARY_ISSUER_ID" \
-# --key-id "$MACOS_NOTARY_KEY_ID" \
-# --key "$NOTARY_KEY_FILE" \
-# --wait
-
-echo "Attach staple"
-xcrun stapler staple "$APP_PATH"
diff --git a/scripts/sign-macos-old.sh b/scripts/sign-macos-old.sh
new file mode 100755
index 00000000..3b1c1620
--- /dev/null
+++ b/scripts/sign-macos-old.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env -S bash -e
+
+ENTITLEMENTS_PATH="assets/entitlements.plist"
+
+APP_BUNDLE_PATH="${APP_BUNDLE_PATH:?APP_BUNDLE_PATH not set}"
+
+# 1. Create a temporary keychain and import certificate
+KEYCHAIN=build.keychain-db
+
+if security list-keychains | grep -q "$KEYCHAIN"; then
+ echo "Keychain $KEYCHAIN already exists, using existing keychain."
+else
+ security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
+fi
+
+security default-keychain -s "$KEYCHAIN"
+security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
+security set-keychain-settings "$KEYCHAIN"
+security default-keychain -s "$KEYCHAIN"
+security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
+security set-keychain-settings "$KEYCHAIN"
+
+echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
+security import certificate.p12 \
+ -k "$KEYCHAIN" \
+ -P "$MACOS_CERTIFICATE_PWD" \
+ -T /usr/bin/codesign
+
+security set-key-partition-list -S apple-tool:,apple:,codesign: \
+ -s -k "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
+
+# 2. Sign app bundle
+codesign --deep --force --options runtime --timestamp \
+ --entitlements $ENTITLEMENTS_PATH \
+ --sign "$MACOS_CERTIFICATE_NAME" \
+ "$APP_BUNDLE_PATH"
+
+codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE_PATH"
+echo "Signed app at $APP_BUNDLE_PATH"
diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh
index 3b1c1620..0cd5d814 100755
--- a/scripts/sign-macos.sh
+++ b/scripts/sign-macos.sh
@@ -1,39 +1,95 @@
-#!/usr/bin/env -S bash -e
+#!/bin/bash
+set -euo pipefail
-ENTITLEMENTS_PATH="assets/entitlements.plist"
+RELEASE_DIR="target/release"
+APP_DIR="$RELEASE_DIR/macos"
+APP_NAME="Rustcast.app"
+APP_PATH="$APP_DIR/$APP_NAME"
-APP_BUNDLE_PATH="${APP_BUNDLE_PATH:?APP_BUNDLE_PATH not set}"
+# --- Required env vars (using the names you provided) ---
+environment=(
+ "MACOS_CERTIFICATE"
+ "MACOS_CERTIFICATE_PWD"
+ "MACOS_CI_KEYCHAIN_PWD"
+ "MACOS_CERTIFICATE_NAME"
+ "MACOS_NOTARIZATION_PWD"
+ "MACOS_NOTARY_TEAM_ID"
+ "MACOS_NOTARY_KEY_ID"
+ "MACOS_NOTARY_KEY"
+)
-# 1. Create a temporary keychain and import certificate
-KEYCHAIN=build.keychain-db
+for var in "${environment[@]}"; do
+ if [[ -z "${!var:-}" ]]; then
+ echo "Error: $var is not set"
+ exit 1
+ fi
+done
-if security list-keychains | grep -q "$KEYCHAIN"; then
- echo "Keychain $KEYCHAIN already exists, using existing keychain."
+# Optional: only needed if you still want to keep this around
+: "${MACOS_NOTARISATION_APPLE_ID:=}"
+
+echo "Decoding certificate"
+echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
+
+echo "Installing cert in a new keychain"
+security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+security default-keychain -s build.keychain
+security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
+security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+
+echo "Signing..."
+/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime --timestamp "$APP_PATH" -v
+
+/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
+spctl --assess --type execute --verbose "$APP_PATH"
+
+echo "Creating temp notarization archive"
+ditto -c -k --keepParent "$APP_PATH" "notarization.zip"
+
+SUBMIT_JSON=$(xcrun notarytool submit "notarization.zip" \
+ --key "$NOTARY_KEY_FILE" \
+ --key-id "$MACOS_NOTARY_KEY_ID" \
+ --issuer "$MACOS_NOTARY_ISSUER_ID" \
+ --output-format json)
+
+echo "$SUBMIT_JSON"
+
+SUBMIT_ID=$(echo "$SUBMIT_JSON" | jq -r .id)
+
+WAIT_STATUS=0
+
+xcrun notarytool wait "$SUBMIT_ID" \
+ --key "$NOTARY_KEY_FILE" --key-id "$MACOS_NOTARY_KEY_ID" --issuer "$MACOS_NOTARY_ISSUER_ID" \
+ --timeout 30m || WAIT_STATUS=$?
+xcrun notarytool log "$SUBMIT_ID" \
+ --key "$NOTARY_KEY_FILE" --key-id "$MACOS_NOTARY_KEY_ID" --issuer "$MACOS_NOTARY_ISSUER_ID" \
+ notarization-log.json || true
+cat notarization-log.json || true
+if [[ $WAIT_STATUS -ne 0 ]]; then
+ echo "Notarization did not succeed (wait exit $WAIT_STATUS)"
+ exit $WAIT_STATUS
+fi
+
+echo "Notarize app (API key auth)"
+# MACOS_NOTARY_KEY can be either:
+# - the *contents* of the .p8 key, or
+# - base64 of the .p8 key (recommended for CI)
+#
+# If it's base64, decode it first.
+NOTARY_KEY_FILE="AuthKey.p8"
+if printf '%s' "$MACOS_NOTARY_KEY" | grep -q "BEGIN PRIVATE KEY"; then
+ printf '%s' "$MACOS_NOTARY_KEY" > "$NOTARY_KEY_FILE"
else
- security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
+ printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$NOTARY_KEY_FILE"
fi
-security default-keychain -s "$KEYCHAIN"
-security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
-security set-keychain-settings "$KEYCHAIN"
-security default-keychain -s "$KEYCHAIN"
-security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
-security set-keychain-settings "$KEYCHAIN"
+# xcrun notarytool submit "notarization.zip" \
+# --team-id "$MACOS_NOTARY_TEAM_ID" \
+# --issuer "$MACOS_NOTARY_ISSUER_ID" \
+# --key-id "$MACOS_NOTARY_KEY_ID" \
+# --key "$NOTARY_KEY_FILE" \
+# --wait
-echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
-security import certificate.p12 \
- -k "$KEYCHAIN" \
- -P "$MACOS_CERTIFICATE_PWD" \
- -T /usr/bin/codesign
-
-security set-key-partition-list -S apple-tool:,apple:,codesign: \
- -s -k "$MACOS_CI_KEYCHAIN_PWD" "$KEYCHAIN"
-
-# 2. Sign app bundle
-codesign --deep --force --options runtime --timestamp \
- --entitlements $ENTITLEMENTS_PATH \
- --sign "$MACOS_CERTIFICATE_NAME" \
- "$APP_BUNDLE_PATH"
-
-codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE_PATH"
-echo "Signed app at $APP_BUNDLE_PATH"
+echo "Attach staple"
+xcrun stapler staple "$APP_PATH"
From 4b8a6dbf0efdb773d45649bbdab99364529d8c6d Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 23:06:11 +0800
Subject: [PATCH 46/51] turns out order was wrong
---
scripts/sign-macos.sh | 93 ++++++++++++++++++++++++++-----------------
1 file changed, 57 insertions(+), 36 deletions(-)
diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh
index 0cd5d814..774d7559 100755
--- a/scripts/sign-macos.sh
+++ b/scripts/sign-macos.sh
@@ -6,16 +6,16 @@ APP_DIR="$RELEASE_DIR/macos"
APP_NAME="Rustcast.app"
APP_PATH="$APP_DIR/$APP_NAME"
-# --- Required env vars (using the names you provided) ---
+# --- Required env vars ---
environment=(
"MACOS_CERTIFICATE"
"MACOS_CERTIFICATE_PWD"
"MACOS_CI_KEYCHAIN_PWD"
"MACOS_CERTIFICATE_NAME"
- "MACOS_NOTARIZATION_PWD"
"MACOS_NOTARY_TEAM_ID"
"MACOS_NOTARY_KEY_ID"
"MACOS_NOTARY_KEY"
+ "MACOS_NOTARY_ISSUER_ID"
)
for var in "${environment[@]}"; do
@@ -25,28 +25,47 @@ for var in "${environment[@]}"; do
fi
done
-# Optional: only needed if you still want to keep this around
-: "${MACOS_NOTARISATION_APPLE_ID:=}"
+# --- Step 1: Decode the notarization API key FIRST ---
+echo "Preparing notarization API key..."
+NOTARY_KEY_FILE="AuthKey.p8"
+if printf '%s' "$MACOS_NOTARY_KEY" | grep -q "BEGIN PRIVATE KEY"; then
+ printf '%s' "$MACOS_NOTARY_KEY" > "$NOTARY_KEY_FILE"
+else
+ printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$NOTARY_KEY_FILE"
+fi
-echo "Decoding certificate"
+# --- Step 2: Decode and install the signing certificate ---
+echo "Decoding certificate..."
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
-echo "Installing cert in a new keychain"
+echo "Installing cert in a new keychain..."
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
-echo "Signing..."
-/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime --timestamp "$APP_PATH" -v
-
+# --- Step 3: Sign the app ---
+echo "Signing app..."
+/usr/bin/codesign \
+ --force \
+ --deep \
+ --options runtime \
+ --timestamp \
+ -s "$MACOS_CERTIFICATE_NAME" \
+ -v \
+ "$APP_PATH"
+
+# --- Step 4: Verify the signature (not notarization yet) ---
+echo "Verifying signature..."
/usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH"
-spctl --assess --type execute --verbose "$APP_PATH"
-echo "Creating temp notarization archive"
+# --- Step 5: Create notarization zip ---
+echo "Creating notarization archive..."
ditto -c -k --keepParent "$APP_PATH" "notarization.zip"
+# --- Step 6: Submit for notarization ---
+echo "Submitting for notarization..."
SUBMIT_JSON=$(xcrun notarytool submit "notarization.zip" \
--key "$NOTARY_KEY_FILE" \
--key-id "$MACOS_NOTARY_KEY_ID" \
@@ -54,42 +73,44 @@ SUBMIT_JSON=$(xcrun notarytool submit "notarization.zip" \
--output-format json)
echo "$SUBMIT_JSON"
-
SUBMIT_ID=$(echo "$SUBMIT_JSON" | jq -r .id)
-WAIT_STATUS=0
+if [[ -z "$SUBMIT_ID" || "$SUBMIT_ID" == "null" ]]; then
+ echo "Error: Failed to get submission ID from notarytool"
+ exit 1
+fi
+
+echo "Submission ID: $SUBMIT_ID"
+# --- Step 7: Wait for notarization to complete ---
+echo "Waiting for notarization result..."
+WAIT_STATUS=0
xcrun notarytool wait "$SUBMIT_ID" \
- --key "$NOTARY_KEY_FILE" --key-id "$MACOS_NOTARY_KEY_ID" --issuer "$MACOS_NOTARY_ISSUER_ID" \
+ --key "$NOTARY_KEY_FILE" \
+ --key-id "$MACOS_NOTARY_KEY_ID" \
+ --issuer "$MACOS_NOTARY_ISSUER_ID" \
--timeout 30m || WAIT_STATUS=$?
+
+# --- Step 8: Fetch and print the notarization log ---
+echo "Fetching notarization log..."
xcrun notarytool log "$SUBMIT_ID" \
- --key "$NOTARY_KEY_FILE" --key-id "$MACOS_NOTARY_KEY_ID" --issuer "$MACOS_NOTARY_ISSUER_ID" \
+ --key "$NOTARY_KEY_FILE" \
+ --key-id "$MACOS_NOTARY_KEY_ID" \
+ --issuer "$MACOS_NOTARY_ISSUER_ID" \
notarization-log.json || true
cat notarization-log.json || true
+
if [[ $WAIT_STATUS -ne 0 ]]; then
- echo "Notarization did not succeed (wait exit $WAIT_STATUS)"
+ echo "Notarization did not succeed (wait exit code: $WAIT_STATUS)"
exit $WAIT_STATUS
fi
-echo "Notarize app (API key auth)"
-# MACOS_NOTARY_KEY can be either:
-# - the *contents* of the .p8 key, or
-# - base64 of the .p8 key (recommended for CI)
-#
-# If it's base64, decode it first.
-NOTARY_KEY_FILE="AuthKey.p8"
-if printf '%s' "$MACOS_NOTARY_KEY" | grep -q "BEGIN PRIVATE KEY"; then
- printf '%s' "$MACOS_NOTARY_KEY" > "$NOTARY_KEY_FILE"
-else
- printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$NOTARY_KEY_FILE"
-fi
+# --- Step 9: Staple the notarization ticket ---
+echo "Stapling notarization ticket..."
+xcrun stapler staple "$APP_PATH"
-# xcrun notarytool submit "notarization.zip" \
-# --team-id "$MACOS_NOTARY_TEAM_ID" \
-# --issuer "$MACOS_NOTARY_ISSUER_ID" \
-# --key-id "$MACOS_NOTARY_KEY_ID" \
-# --key "$NOTARY_KEY_FILE" \
-# --wait
+# --- Step 10: Final Gatekeeper check (AFTER stapling) ---
+echo "Running Gatekeeper assessment..."
+spctl --assess --type execute --verbose "$APP_PATH"
-echo "Attach staple"
-xcrun stapler staple "$APP_PATH"
+echo "Done! App is signed, notarized, and stapled."
From 385efed8f885bb3d034cce77562580be5c2cce57 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Wed, 22 Apr 2026 23:55:35 +0800
Subject: [PATCH 47/51] add easter egg for orvar
---
src/app/tile/update.rs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index 1684476d..f9dd999b 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -1008,6 +1008,19 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task {
}];
return single_item_resize_task(id);
}
+ "zombo" => {
+ tile.results = vec![App {
+ ranking: 0,
+ open_command: AppCommand::Function(Function::OpenWebsite(
+ "https://zombo.com".to_string(),
+ )),
+ desc: "Easter Egg".to_string(),
+ icons: None,
+ display_name: "🫳 🌱".to_string(),
+ search_name: "".to_string(),
+ }];
+ return single_item_resize_task(id);
+ }
"lemon" => {
tile.results = vec![App {
ranking: 0,
From 63bff8546cae43c25d8c3434f0591f0afc7f635f Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Fri, 24 Apr 2026 14:34:24 +0800
Subject: [PATCH 48/51] fix: resizing of window when using unit conversion
---
src/app/tile/update.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index f9dd999b..7f1ba3fc 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -1132,7 +1132,7 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task {
.into_iter()
.map(|conversion| conversion.to_app())
.collect();
- return single_item_resize_task(id);
+ return resize_task(id, tile.results.len() as u32);
} else if let Ok(res) = Expr::from_str(&tile.query) {
tile.results.push(App {
ranking: 0,
From a888b2cdcc61c5f6690d05765d0e274600a5bd89 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Fri, 24 Apr 2026 23:19:38 +0800
Subject: [PATCH 49/51] add events opening
---
Cargo.lock | 43 +++++++++++++++++--
Cargo.toml | 3 +-
src/app.rs | 2 +
src/app/apps.rs | 10 +++++
src/app/pages/settings.rs | 28 ++++++++++++-
src/app/tile.rs | 12 ++++++
src/app/tile/elm.rs | 4 ++
src/app/tile/update.rs | 16 ++++++++
src/commands.rs | 14 +++++++
src/config.rs | 6 ++-
src/platform/macos/events.rs | 80 ++++++++++++++++++++++++++++++++++++
src/platform/macos/mod.rs | 1 +
12 files changed, 213 insertions(+), 6 deletions(-)
create mode 100644 src/platform/macos/events.rs
diff --git a/Cargo.lock b/Cargo.lock
index 59d8cc85..ad2b8da1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2866,7 +2866,7 @@ dependencies = [
"bitflags 2.11.0",
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-core-location",
+ "objc2-core-location 0.2.2",
"objc2-foundation 0.2.2",
]
@@ -2975,6 +2975,16 @@ dependencies = [
"objc2-foundation 0.2.2",
]
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.2",
+]
+
[[package]]
name = "objc2-core-text"
version = "0.3.2"
@@ -3006,6 +3016,22 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+[[package]]
+name = "objc2-event-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bc9ad7325642172110196bacd6af64027ec5549ded7fc6589ea03e0f792bf8"
+dependencies = [
+ "bitflags 2.11.0",
+ "block2 0.6.2",
+ "objc2 0.6.3",
+ "objc2-app-kit 0.3.2",
+ "objc2-core-graphics",
+ "objc2-core-location 0.3.2",
+ "objc2-foundation 0.3.2",
+ "objc2-map-kit",
+]
+
[[package]]
name = "objc2-foundation"
version = "0.2.2"
@@ -3055,6 +3081,16 @@ dependencies = [
"objc2-foundation 0.2.2",
]
+[[package]]
+name = "objc2-map-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579edede1c244621cd8229b70ea6e20e6ec3bab5a74afdfd494b446b681e1e64"
+dependencies = [
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.2",
+]
+
[[package]]
name = "objc2-metal"
version = "0.2.2"
@@ -3138,7 +3174,7 @@ dependencies = [
"objc2-cloud-kit 0.2.2",
"objc2-core-data 0.2.2",
"objc2-core-image 0.2.2",
- "objc2-core-location",
+ "objc2-core-location 0.2.2",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core 0.2.2",
@@ -3167,7 +3203,7 @@ dependencies = [
"bitflags 2.11.0",
"block2 0.5.1",
"objc2 0.5.2",
- "objc2-core-location",
+ "objc2-core-location 0.2.2",
"objc2-foundation 0.2.2",
]
@@ -3868,6 +3904,7 @@ dependencies = [
"objc2-app-kit 0.3.2",
"objc2-application-services",
"objc2-core-foundation",
+ "objc2-event-kit",
"objc2-foundation 0.3.2",
"objc2-service-management",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 7cb8af64..bd6362b8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,8 @@ objc2 = "0.6.3"
objc2-app-kit = { version = "0.3.2", features = ["NSImage"] }
objc2-application-services = { version = "0.3.2", default-features = false, features = ["HIServices", "Processes"] }
objc2-core-foundation = "0.3.2"
-objc2-foundation = { version = "0.3.2", features = ["NSString"] }
+objc2-event-kit = "0.3.2"
+objc2-foundation = { version = "0.3.2", features = ["NSDateFormatter", "NSFormatter", "NSString"] }
objc2-service-management = "0.3.2"
once_cell = "1.21.3"
rand = "0.9.2"
diff --git a/src/app.rs b/src/app.rs
index 6892bdb5..99bd510a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -101,6 +101,7 @@ pub enum Message {
OpenFileDialogue(String),
ReturnFocus,
EscKeyPressed(Id),
+ UpdateEvents,
ClearSearchResults,
WindowFocusChanged(Id, bool),
ClearSearchQuery,
@@ -131,6 +132,7 @@ pub enum SetConfigFields {
HapticFeedback(bool),
ShowMenubarIcon(bool),
SetPage(MainPage),
+ SetEventDuration(String),
Modes(Editable<(String, String)>),
Aliases(Editable<(String, String)>),
SearchDirs(Editable),
diff --git a/src/app/apps.rs b/src/app/apps.rs
index 90d615cc..9a176816 100644
--- a/src/app/apps.rs
+++ b/src/app/apps.rs
@@ -59,6 +59,16 @@ impl PartialEq for App {
}
impl App {
+ pub fn new(name: String, icon: Option, desc: String, command: AppCommand) -> Self {
+ Self {
+ ranking: 0,
+ open_command: command,
+ icons: icon,
+ search_name: name.to_lowercase(),
+ display_name: name,
+ desc,
+ }
+ }
/// A vec of all the emojis as App structs
pub fn emoji_apps() -> Vec {
emojis::iter()
diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs
index 2c59db10..a0b77d26 100644
--- a/src/app/pages/settings.rs
+++ b/src/app/pages/settings.rs
@@ -175,7 +175,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
})
.into(),
radio(
- "Frequently Used",
+ "Frequents",
MainPage::FrequentlyUsed,
Some(config.main_page),
|page| Message::SetConfig(SetConfigFields::SetPage(page)),
@@ -185,6 +185,14 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
move |_, _| settings_radio_button_style(&theme_clone.clone())
})
.into(),
+ radio("Events", MainPage::Events, Some(config.main_page), |page| {
+ Message::SetConfig(SetConfigFields::SetPage(page))
+ })
+ .style({
+ let theme_clone = theme_clone.clone();
+ move |_, _| settings_radio_button_style(&theme_clone.clone())
+ })
+ .into(),
radio("Nothing", MainPage::Blank, Some(config.main_page), |page| {
Message::SetConfig(SetConfigFields::SetPage(page))
})
@@ -277,6 +285,23 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
notice_item(theme.clone(), "What font rustcast should use"),
]);
+ let theme_clone = theme.clone();
+ let event_duration = settings_item_column([
+ settings_hint_text(theme.clone(), "Set Event duration"),
+ text_input("Event duration", &config.event_duration.to_string())
+ .on_input(move |input: String| {
+ Message::SetConfig(SetConfigFields::SetEventDuration(input))
+ })
+ .on_submit(Message::WriteConfig(false))
+ .width(Length::Fill)
+ .style(move |_, _| settings_text_input_item_style(&theme_clone))
+ .into(),
+ notice_item(
+ theme.clone(),
+ "How many minutes from now the events should be displayed",
+ ),
+ ]);
+
let theme_clone = theme.clone();
let theme_clone_1 = theme.clone();
let theme_clone_2 = theme.clone();
@@ -431,6 +456,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
clear_on_enter.into(),
show_icons.into(),
font_family.into(),
+ event_duration.into(),
text_clr.into(),
bg_clr.into(),
settings_hint_text(theme.clone(), "Aliases"),
diff --git a/src/app/tile.rs b/src/app/tile.rs
index 7b131bee..62759d10 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -8,6 +8,7 @@ use crate::clipboard::ClipBoardContentType;
use crate::config::{Config, Shelly};
use crate::debounce::Debouncer;
use crate::platform::default_app_paths;
+use crate::platform::macos::events::Event;
use crate::platform::macos::launching::Shortcut;
use arboard::Clipboard;
@@ -173,6 +174,7 @@ pub struct Tile {
emoji_apps: AppIndex,
visible: bool,
focused: bool,
+ pub events: Vec,
frontmost: Option>,
pub config: Config,
hotkeys: Hotkeys,
@@ -233,6 +235,7 @@ impl Tile {
keyboard,
Subscription::run(crate::platform::macos::urlscheme::url_stream),
Subscription::run(handle_recipient),
+ Subscription::run(reload_events),
Subscription::run(handle_version_and_rankings),
Subscription::run(handle_clipboard_history),
Subscription::run(handle_file_search),
@@ -595,6 +598,15 @@ fn handle_recipient() -> impl futures::Stream- {
})
}
+fn reload_events() -> impl futures::Stream
- {
+ stream::channel(100, async |mut output| {
+ loop {
+ output.send(Message::UpdateEvents).await.ok();
+ tokio::time::sleep(Duration::from_mins(2)).await;
+ }
+ })
+}
+
fn handle_version_and_rankings() -> impl futures::Stream
- {
stream::channel(100, async |mut output| {
let current_version = format!("\"{}\"", option_env!("APP_VERSION").unwrap_or(""));
diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs
index 7fbe2ab9..0e1db6b2 100644
--- a/src/app/tile/elm.rs
+++ b/src/app/tile/elm.rs
@@ -22,6 +22,7 @@ use crate::app::tile::{AppIndex, Hotkeys};
use crate::app::{DEFAULT_WINDOW_HEIGHT, ToApp, ToApps};
use crate::config::Theme;
use crate::debounce::Debouncer;
+use crate::platform::macos::events::Event;
use crate::styles::{
contents_style, glass_border, glass_surface, results_scrollbar_style, rustcast_text_input_style,
};
@@ -38,6 +39,8 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) {
let (id, open) = window::open(default_settings());
info!("Opening window");
+ let events = Event::get_events(config.event_duration);
+
let open = open.discard().chain(window::run(id, |handle| {
platform::window_config(&handle.window_handle().expect("Unable to get window handle"));
transform_process_to_ui_element();
@@ -78,6 +81,7 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) {
results: vec![],
options,
hotkeys,
+ events,
emoji_apps: AppIndex::from_apps(App::emoji_apps()),
visible: true,
frontmost: None,
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index f9dd999b..41c1db1d 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -36,6 +36,7 @@ use crate::commands::Function;
use crate::config::Config;
use crate::config::MainPage;
use crate::debounce::DebouncePolicy;
+use crate::platform::macos::events::Event;
use crate::platform::macos::launching::Shortcut;
use crate::platform::macos::launching::global_handler;
use crate::platform::macos::{start_at_login, stop_at_login};
@@ -71,6 +72,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
}
}
+ Message::UpdateEvents => {
+ tile.events = Event::get_events(tile.config.event_duration);
+ Task::none()
+ }
+
Message::UriReceived(uri) => {
let Ok(url) = Url::parse(&uri) else {
return Task::none();
@@ -706,6 +712,15 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
SetConfigFields::Modes(Editable::Create((key, value))) => {
final_config.modes.insert(key, value);
}
+ SetConfigFields::SetEventDuration(duration) => {
+ if duration.trim().is_empty() {
+ final_config.event_duration = 0;
+ } else if let Ok(duration) = duration.parse::() {
+ final_config.event_duration = duration;
+ }
+
+ tile.events = Event::get_events(final_config.event_duration);
+ }
SetConfigFields::Modes(Editable::Delete((key, _))) => {
final_config.modes.remove(&key);
}
@@ -976,6 +991,7 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task {
if tile.page == Page::Main && tile.query_lc.is_empty() {
tile.results = match tile.config.main_page {
MainPage::FrequentlyUsed => tile.frequent_results(),
+ MainPage::Events => tile.events.iter().map(|x| x.to_app()).collect(),
MainPage::Blank => vec![],
MainPage::Favourites => tile.options.get_favourites(),
};
diff --git a/src/commands.rs b/src/commands.rs
index a4d6d29a..285ef368 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -19,6 +19,7 @@ use crate::{
pub enum Function {
OpenApp(String),
QuitApp(String),
+ OpenRawUrl(String),
QuitAllApps,
RunShellCommand(String),
OpenWebsite(String),
@@ -41,6 +42,19 @@ impl Function {
));
});
}
+
+ Function::OpenRawUrl(url) => {
+ let url = url.to_owned();
+ thread::spawn(move || {
+ NSWorkspace::new().openURL(
+ &NSURL::URLWithString_relativeToURL(
+ &objc2_foundation::NSString::from_str(&url),
+ None,
+ )
+ .unwrap(),
+ );
+ });
+ }
Function::RunShellCommand(command) => {
Command::new("sh").arg("-c").arg(command).spawn().ok();
}
diff --git a/src/config.rs b/src/config.rs
index 41de2496..7aa46f52 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -20,6 +20,7 @@ pub struct Config {
pub toggle_hotkey: String,
pub clipboard_hotkey: String,
pub buffer_rules: Buffer,
+ pub event_duration: u32,
pub main_page: MainPage,
pub start_at_login: bool,
pub theme: Theme,
@@ -45,6 +46,7 @@ impl Default for Config {
buffer_rules: Buffer::default(),
theme: Theme::default(),
start_at_login: true,
+ event_duration: 60,
placeholder: String::from("Time to be productive!"),
search_url: "https://duckduckgo.com/search?q=%s".to_string(),
cbhist: true,
@@ -66,6 +68,7 @@ impl Default for Config {
pub enum MainPage {
Favourites,
FrequentlyUsed,
+ Events,
#[default]
Blank,
}
@@ -73,9 +76,10 @@ pub enum MainPage {
impl std::fmt::Display for MainPage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
- MainPage::Blank => "♥️ Rustcast",
+ MainPage::Blank => "Rustcast",
MainPage::Favourites => "Favourites",
MainPage::FrequentlyUsed => "Frequently Used",
+ MainPage::Events => "Events",
})
}
}
diff --git a/src/platform/macos/events.rs b/src/platform/macos/events.rs
new file mode 100644
index 00000000..a91373b3
--- /dev/null
+++ b/src/platform/macos/events.rs
@@ -0,0 +1,80 @@
+use block2::RcBlock;
+use objc2::runtime::Bool;
+use objc2_event_kit::{EKEntityType, EKEventStore};
+use objc2_foundation::{NSDate, NSDateFormatter, NSDateFormatterStyle, NSError};
+
+use crate::{
+ app::{
+ ToApp,
+ apps::{App, AppCommand, ICNS_ICON},
+ },
+ commands::Function,
+ utils::icns_data_to_handle,
+};
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Event {
+ pub event_name: String,
+ pub event_url: Option,
+ pub time: String,
+}
+
+impl ToApp for Event {
+ fn to_app(&self) -> App {
+ let icons = icns_data_to_handle(ICNS_ICON.to_vec());
+ let appcmd = if let Some(url) = &self.event_url {
+ AppCommand::Function(Function::OpenRawUrl(url.clone()))
+ } else {
+ AppCommand::Display
+ };
+ App::new(self.event_name.clone(), icons, self.time.clone(), appcmd)
+ }
+}
+
+impl Event {
+ pub fn get_events(duration_in_min: u32) -> Vec {
+ unsafe {
+ let store = EKEventStore::new();
+
+ let (tx, rx) = std::sync::mpsc::channel::<()>();
+
+ let block = block2::RcBlock::new(move |_: Bool, _: *mut NSError| {
+ let _ = tx.send(());
+ });
+
+ store.requestFullAccessToEventsWithCompletion(RcBlock::<
+ dyn std::ops::Fn(objc2::runtime::Bool, *mut NSError),
+ >::as_ptr(&block));
+
+ rx.recv().unwrap();
+
+ let start = NSDate::now();
+ let end = NSDate::dateWithTimeIntervalSinceNow((duration_in_min * 60) as f64);
+
+ let calendars = store.calendarsForEntityType(EKEntityType::Event);
+
+ let predicate = store.predicateForEventsWithStartDate_endDate_calendars(
+ &start,
+ &end,
+ Some(&calendars),
+ );
+
+ let formatter = NSDateFormatter::new();
+
+ formatter.setDateStyle(NSDateFormatterStyle::MediumStyle);
+ formatter.setTimeStyle(NSDateFormatterStyle::ShortStyle);
+
+ store
+ .eventsMatchingPredicate(&predicate)
+ .iter()
+ .map(|x| Event {
+ event_name: x.title().to_string(),
+ event_url: x
+ .URL()
+ .and_then(|url| url.absoluteString().map(|x| x.to_string())),
+ time: formatter.stringFromDate(&x.startDate()).to_string(),
+ })
+ .collect()
+ }
+ }
+}
diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs
index 4a6a340e..ff8cc138 100644
--- a/src/platform/macos/mod.rs
+++ b/src/platform/macos/mod.rs
@@ -1,6 +1,7 @@
//! Macos specific logic, such as window settings, etc.
pub mod accessibility;
pub mod discovery;
+pub mod events;
pub mod haptics;
pub mod launching;
pub mod urlscheme;
From 1093f6d2f3abcc4831c188d45e5c3cb9902d8aad Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Sat, 25 Apr 2026 02:26:25 +0800
Subject: [PATCH 50/51] add auto updating
---
Cargo.lock | 547 +++++++++++++++++++++++++++++++++++++-
Cargo.toml | 4 +
src/app.rs | 1 +
src/app/pages/settings.rs | 14 +
src/app/tile.rs | 43 +--
src/app/tile/update.rs | 12 +
src/autoupdate.rs | 222 ++++++++++++++++
src/config.rs | 2 +
src/main.rs | 1 +
9 files changed, 805 insertions(+), 41 deletions(-)
create mode 100644 src/autoupdate.rs
diff --git a/Cargo.lock b/Cargo.lock
index 59d8cc85..63fbcaf0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -30,6 +30,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
[[package]]
name = "ahash"
version = "0.8.12"
@@ -454,6 +465,24 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+dependencies = [
+ "hybrid-array",
+]
+
[[package]]
name = "block2"
version = "0.5.1"
@@ -535,6 +564,15 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+[[package]]
+name = "bzip2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
+dependencies = [
+ "libbz2-rs-sys",
+]
+
[[package]]
name = "cairo-rs"
version = "0.18.5"
@@ -651,6 +689,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common 0.1.7",
+ "inout",
+]
+
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -726,6 +774,18 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -829,6 +889,24 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -891,6 +969,25 @@ dependencies = [
"wgpu",
]
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
+dependencies = [
+ "hybrid-array",
+]
+
[[package]]
name = "ctor-lite"
version = "0.1.2"
@@ -913,6 +1010,43 @@ dependencies = [
"byteorder",
]
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
+dependencies = [
+ "block-buffer 0.12.0",
+ "const-oid",
+ "crypto-common 0.2.1",
+]
+
[[package]]
name = "dirs"
version = "6.0.0"
@@ -1191,6 +1325,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.9",
+ "zlib-rs",
]
[[package]]
@@ -1432,6 +1567,16 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -1461,10 +1606,25 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
- "r-efi",
+ "r-efi 5.3.0",
"wasip2",
]
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
[[package]]
name = "gif"
version = "0.14.1"
@@ -1806,6 +1966,24 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "iced"
version = "0.14.0"
@@ -2085,6 +2263,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
[[package]]
name = "idna"
version = "1.1.0"
@@ -2154,6 +2338,17 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
]
[[package]]
@@ -2277,6 +2472,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
[[package]]
name = "lebe"
version = "0.5.3"
@@ -2307,6 +2508,12 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "libbz2-rs-sys"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f"
+
[[package]]
name = "libc"
version = "0.2.182"
@@ -2448,6 +2655,15 @@ version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
+[[package]]
+name = "lzma-rust2"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
+dependencies = [
+ "sha2 0.10.9",
+]
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -2714,6 +2930,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -3287,6 +3509,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest 0.10.7",
+ "hmac",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -3442,6 +3674,18 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppmd-rust"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -3457,6 +3701,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -3586,6 +3840,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
[[package]]
name = "rand"
version = "0.9.2"
@@ -3858,6 +4118,7 @@ dependencies = [
"crossbeam-channel",
"emojis",
"global-hotkey",
+ "hex",
"iced",
"icns",
"image",
@@ -3876,11 +4137,14 @@ dependencies = [
"rfd",
"serde",
"serde_json",
+ "sha2 0.11.0",
+ "tempfile",
"tokio",
"toml 0.9.12+spec-1.1.0",
"tracing-subscriber",
"tray-icon",
"url",
+ "zip",
]
[[package]]
@@ -4065,6 +4329,39 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.2",
+]
+
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -4282,6 +4579,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
[[package]]
name = "svg_fmt"
version = "0.4.5"
@@ -4444,6 +4747,26 @@ dependencies = [
"zune-jpeg 0.4.21",
]
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "js-sys",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
[[package]]
name = "tiny-skia"
version = "0.11.4"
@@ -4723,6 +5046,18 @@ dependencies = [
"core_maths",
]
+[[package]]
+name = "typed-path"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
+
+[[package]]
+name = "typenum"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+
[[package]]
name = "uds_windows"
version = "1.2.1"
@@ -4770,6 +5105,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -4858,6 +5199,15 @@ dependencies = [
"wit-bindgen",
]
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
[[package]]
name = "wasm-bindgen"
version = "0.2.113"
@@ -4917,6 +5267,40 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
[[package]]
name = "wasmtimer"
version = "0.4.3"
@@ -5774,6 +6158,88 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.0",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
[[package]]
name = "writeable"
@@ -5997,6 +6463,12 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
[[package]]
name = "zerotrie"
version = "0.2.3"
@@ -6030,12 +6502,85 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "zip"
+version = "8.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59"
+dependencies = [
+ "aes",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "deflate64",
+ "flate2",
+ "getrandom 0.4.2",
+ "hmac",
+ "indexmap",
+ "lzma-rust2",
+ "memchr",
+ "pbkdf2",
+ "ppmd-rust",
+ "sha1",
+ "time",
+ "typed-path",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
[[package]]
name = "zune-core"
version = "0.4.12"
diff --git a/Cargo.toml b/Cargo.toml
index 7cb8af64..367e842a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ block2 = "0.6.2"
crossbeam-channel = "0.5.15"
emojis = "0.8.0"
global-hotkey = "0.7.0"
+hex = "0.4.3"
iced = { version = "0.14.0", features = ["image", "tokio"] }
icns = "0.3.1"
image = { version = "0.25.9", features = ["tiff"] }
@@ -31,8 +32,11 @@ rayon = "1.11.0"
rfd = "0.17.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
+sha2 = "0.11.0"
+tempfile = "3.27.0"
tokio = { version = "1.48.0", features = ["full"] }
toml = "0.9.8"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tray-icon = "0.21.3"
url = { version = "2.5.8", default-features = false }
+zip = "8.5.1"
diff --git a/src/app.rs b/src/app.rs
index 6892bdb5..9f683f06 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -128,6 +128,7 @@ pub enum SetConfigFields {
PlaceHolder(String),
SearchUrl(String),
ClipboardHistory(bool),
+ SetAutoUpdate(bool),
HapticFeedback(bool),
ShowMenubarIcon(bool),
SetPage(MainPage),
diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs
index 2c59db10..01d81493 100644
--- a/src/app/pages/settings.rs
+++ b/src/app/pages/settings.rs
@@ -129,6 +129,19 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
notice_item(theme.clone(), "If you want rustcast to start on login"),
]);
+ let theme_clone = theme.clone();
+ let auto_update = settings_item_row([
+ settings_hint_text(theme.clone(), "Auto update"),
+ checkbox(config.clone().auto_update)
+ .style(move |_, _| settings_checkbox_style(&theme_clone))
+ .on_toggle(move |input| Message::SetConfig(SetConfigFields::SetAutoUpdate(input)))
+ .into(),
+ notice_item(
+ theme.clone(),
+ "If rustcast should automatically update itself",
+ ),
+ ]);
+
let theme_clone = theme.clone();
let haptic = Row::from_iter([
settings_hint_text(theme.clone(), "Haptic feedback"),
@@ -422,6 +435,7 @@ pub fn settings_page(config: Config) -> Element<'static, Message> {
search.into(),
debounce.into(),
start_at_login.into(),
+ auto_update.into(),
haptic.into(),
tray_icon.into(),
clipboard_history.into(),
diff --git a/src/app/tile.rs b/src/app/tile.rs
index 7b131bee..ce248e30 100644
--- a/src/app/tile.rs
+++ b/src/app/tile.rs
@@ -4,6 +4,7 @@ pub mod update;
use crate::app::apps::App;
use crate::app::{ArrowKey, Message, Move, Page};
+use crate::autoupdate::new_version_available;
use crate::clipboard::ClipBoardContentType;
use crate::config::{Config, Shelly};
use crate::debounce::Debouncer;
@@ -32,7 +33,6 @@ use tray_icon::TrayIcon;
use std::collections::HashMap;
use std::fmt::Debug;
-use std::str::FromStr;
use std::time::Duration;
/// This is a wrapper around the sender to disable dropping
@@ -597,46 +597,9 @@ fn handle_recipient() -> impl futures::Stream
- {
fn handle_version_and_rankings() -> impl futures::Stream
- {
stream::channel(100, async |mut output| {
- let current_version = format!("\"{}\"", option_env!("APP_VERSION").unwrap_or(""));
-
- if current_version.is_empty() {
- println!("empty version");
- return;
- }
-
- let req = minreq::Request::new(
- minreq::Method::Get,
- "https://api.github.com/repos/RustCastLabs/rustcast/releases/latest",
- )
- .with_header("User-Agent", "rustcast-update-checker")
- .with_header("Accept", "application/vnd.github+json")
- .with_header("X-GitHub-Api-Version", "2022-11-28");
-
loop {
- let resp = req
- .clone()
- .send()
- .and_then(|x| x.as_str().map(serde_json::Value::from_str));
-
- info!("Made a req for latest version");
-
- if let Ok(Ok(val)) = resp {
- let new_ver = val
- .get("name")
- .map(|x| x.to_string())
- .unwrap_or("".to_string());
-
- // new_ver is in the format "\"v0.0.0\""
- // note that it is encapsulated in double quotes
- if new_ver.trim() != current_version
- && !new_ver.is_empty()
- && new_ver.starts_with("\"v")
- {
- info!("new version available: {new_ver}");
- output.send(Message::UpdateAvailable).await.ok();
- }
- } else {
- warn!("Error getting resp");
+ if new_version_available().is_some() {
+ output.send(Message::UpdateAvailable).await.ok();
}
tokio::time::sleep(Duration::from_secs(30)).await;
output.send(Message::SaveRanking).await.ok();
diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs
index f9dd999b..7b3407ba 100644
--- a/src/app/tile/update.rs
+++ b/src/app/tile/update.rs
@@ -31,6 +31,8 @@ use crate::app::menubar::menu_builder;
use crate::app::menubar::menu_icon;
use crate::app::tile::AppIndex;
use crate::app::{Message, Page, tile::Tile};
+use crate::autoupdate::download_latest_app;
+use crate::autoupdate::relaunch_app;
use crate::calculator::Expr;
use crate::commands::Function;
use crate::config::Config;
@@ -96,6 +98,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
Message::UpdateAvailable => {
tile.update_available = true;
+
+ if tile.config.auto_update {
+ thread::spawn(|| {
+ download_latest_app().ok();
+ relaunch_app();
+ });
+ }
Task::done(Message::ReloadConfig)
}
@@ -796,6 +805,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task {
SetConfigFields::HapticFeedback(haptic_feedback) => {
final_config.haptic_feedback = haptic_feedback
}
+ SetConfigFields::SetAutoUpdate(au) => {
+ final_config.auto_update = au;
+ }
SetConfigFields::ShowMenubarIcon(show) => final_config.show_trayicon = show,
SetConfigFields::SetThemeFields(SetConfigThemeFields::Font(fnt)) => {
final_config.theme.font = Some(fnt)
diff --git a/src/autoupdate.rs b/src/autoupdate.rs
new file mode 100644
index 00000000..de34d7b8
--- /dev/null
+++ b/src/autoupdate.rs
@@ -0,0 +1,222 @@
+use std::str::FromStr;
+
+use log::{error, info};
+use sha2::{Digest, Sha256};
+
+pub struct ReleaseInfo {
+ pub version: String,
+ pub zip_url: String,
+ pub sha256: String,
+}
+
+pub fn get_latest_release() -> Option {
+ let req = minreq::Request::new(
+ minreq::Method::Get,
+ "https://api.github.com/repos/RustCastLabs/rustcast/releases/latest",
+ )
+ .with_header("User-Agent", "rustcast-update-checker")
+ .with_header("Accept", "application/vnd.github+json")
+ .with_header("X-GitHub-Api-Version", "2022-11-28");
+
+ let resp = req
+ .send()
+ .and_then(|x| x.as_str().map(serde_json::Value::from_str));
+
+ if let Ok(Ok(val)) = resp {
+ let version = val.get("name")?.as_str()?.to_string();
+
+ let assets = val.get("assets")?.as_array()?;
+
+ let mut zip_url = None;
+ let mut sha256 = None;
+
+ for asset in assets {
+ let name = asset.get("name")?.as_str()?;
+ let url = asset.get("browser_download_url")?.as_str()?.to_string();
+
+ if name == "Rustcast-universal-macos.app.zip" {
+ zip_url = Some(url);
+
+ sha256 = asset
+ .get("digest")
+ .and_then(|d| d.as_str())
+ .and_then(|d| d.strip_prefix("sha256:"))
+ .map(|d| d.to_string());
+ }
+ }
+
+ Some(ReleaseInfo {
+ version,
+ zip_url: zip_url?,
+ sha256: sha256?,
+ })
+ } else {
+ None
+ }
+}
+
+pub fn new_version_available() -> Option {
+ info!("Checking for new version");
+ let info = get_latest_release()?;
+ info!("Got latest info");
+ let current = option_env!("APP_VERSION").unwrap_or("");
+
+ if info.version != current {
+ Some(info)
+ } else {
+ None
+ }
+}
+
+pub fn verify_sha256(file_path: &std::path::Path, expected_hex: &str) -> std::io::Result {
+ let bytes = std::fs::read(file_path)?;
+ let digest = Sha256::digest(&bytes);
+ let actual_hex = hex::encode(digest);
+ Ok(actual_hex == expected_hex)
+}
+
+pub fn download_latest_app() -> Result {
+ let info = get_latest_release().ok_or_else(|| {
+ error!("Could not get latest release info");
+ })?;
+
+ info!("got latest release");
+
+ let tmp = tempfile::tempdir().map_err(|e| {
+ error!("Could not create temporary directory: {e}");
+ })?;
+
+ info!("created temp dir");
+
+ let zip_path = tmp.path().join("Rustcast-universal-macos.app.zip");
+
+ info!("zip path: {:?}", zip_path);
+ let resp = minreq::get(&info.zip_url)
+ .with_header("User-Agent", "rustcast-update-checker")
+ .send()
+ .map_err(|e| {
+ error!("Could not download update: {e}");
+ })?;
+
+ info!("downloaded zip");
+
+ std::fs::write(&zip_path, resp.as_bytes()).map_err(|e| {
+ error!("Could not write zip to disk: {e}");
+ })?;
+
+ info!("wrote zip to disk");
+
+ let ok = verify_sha256(&zip_path, &info.sha256).map_err(|e| {
+ error!("Could not verify sha256: {e}");
+ })?;
+
+ info!("verified sha256");
+
+ if !ok {
+ error!("SHA256 mismatch — aborting update");
+ return Err(());
+ }
+
+ let zip_file = std::fs::File::open(&zip_path).map_err(|e| {
+ error!("Could not open zip: {e}");
+ })?;
+
+ info!("opened zip");
+
+ let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| {
+ error!("Could not read zip archive: {e}");
+ })?;
+
+ info!("read zip archive. contents:");
+
+ archive.extract(tmp.path()).map_err(|e| {
+ error!("Could not extract zip: {e}");
+ })?;
+
+ if let Ok(entries) = std::fs::read_dir(tmp.path()) {
+ for entry in entries.flatten() {
+ info!(" extracted entry: {:?}", entry.file_name());
+ }
+ }
+
+ let extracted_app = tmp.path().join("target/release/macos/Rustcast.app");
+
+ info!("found extracted app at: {:?}", extracted_app);
+
+ let dest = get_app_path().ok_or_else(|| {
+ error!("Could not determine current app path");
+ })?;
+
+ info!("Installing update over {:?}", dest);
+
+ if dest.exists() {
+ std::fs::remove_dir_all(&dest).map_err(|e| {
+ error!("Could not remove existing app: {e}");
+ })?;
+ }
+
+ move_or_copy(&extracted_app, &dest).map_err(|e| {
+ error!("Could not move app into place: {e}");
+ })?;
+
+ info!("Successful update");
+
+ Ok(dest)
+}
+
+fn move_or_copy(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
+ match std::fs::rename(src, dst) {
+ Ok(()) => Ok(()),
+ Err(_) => {
+ copy_dir_recursive(src, dst)?;
+ std::fs::remove_dir_all(src)
+ }
+ }
+}
+
+fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
+ std::fs::create_dir_all(dst)?;
+ for entry in std::fs::read_dir(src)? {
+ let entry = entry?;
+ let dst_path = dst.join(entry.file_name());
+ if entry.file_type()?.is_dir() {
+ copy_dir_recursive(&entry.path(), &dst_path)?;
+ } else {
+ std::fs::copy(entry.path(), dst_path)?;
+ }
+ }
+ Ok(())
+}
+
+pub fn relaunch_app() {
+ let app_path = match get_app_path() {
+ Some(p) => p,
+ None => {
+ error!("Could not determine current app path for relaunch");
+ return;
+ }
+ };
+
+ match std::process::Command::new("open").arg(&app_path).spawn() {
+ Ok(_) => {
+ info!("Relaunching app at {:?}", app_path);
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ std::process::exit(0);
+ }
+ Err(e) => {
+ error!("Could not relaunch app: {e}");
+ }
+ }
+}
+
+pub fn get_app_path() -> Option {
+ let exe = std::env::current_exe().ok()?;
+
+ let mut path = exe.as_path();
+ loop {
+ if path.extension().and_then(|e| e.to_str()) == Some("app") {
+ return Some(path.to_path_buf());
+ }
+ path = path.parent()?;
+ }
+}
diff --git a/src/config.rs b/src/config.rs
index 41de2496..d038e093 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -34,6 +34,7 @@ pub struct Config {
pub search_dirs: Vec,
pub log_path: String,
pub debounce_delay: u64,
+ pub auto_update: bool,
}
impl Default for Config {
@@ -49,6 +50,7 @@ impl Default for Config {
search_url: "https://duckduckgo.com/search?q=%s".to_string(),
cbhist: true,
haptic_feedback: false,
+ auto_update: true,
show_trayicon: true,
main_page: MainPage::default(),
search_dirs: vec!["~".to_string()],
diff --git a/src/main.rs b/src/main.rs
index 82c88636..c10192cf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
#![deny(clippy::dbg_macro)]
mod app;
+mod autoupdate;
mod calculator;
mod clipboard;
mod commands;
From d814b5741a42df65f1d340f1eed41659d086d276 Mon Sep 17 00:00:00 2001
From: unsecretised
Date: Mon, 27 Apr 2026 02:48:39 +0800
Subject: [PATCH 51/51] Update stats
---
docs/index.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/index.html b/docs/index.html
index f1b56293..997fce82 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -75,8 +75,8 @@
The fastest launcher
you'll ever use
-
550+ stars
-
1.9k+ downloads
+
590+ stars
+
2.3k+ downloads
100% free forever
MIT license