Skip to content

Commit b4aaa38

Browse files
authored
Merge branch 'windows-support' into minor-fixes
2 parents 404b352 + a9aee4d commit b4aaa38

12 files changed

Lines changed: 495 additions & 17 deletions

File tree

debian/changelog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
rustcast (0.5.2) unstable; urgency=medium
2+
3+
* Fix app discovery: [Merge pull request #149 from unsecretised/fix-app…]
4+
5+
-- Umang Surana <no1umang@gmail.com> Fri, 13 Feb 2026 07:23:44 +0200

debian/control

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Source: rustcast
2+
Section: utils
3+
Priority: optional
4+
Maintainer: Umang Surana
5+
Build-Depends: debhelper-compat (= 13), pkg-config, libgtk-3-dev, libssl-dev, libayatana-appindicator3-dev
6+
Standards-Version: 4.5.0
7+
8+
Package: rustcast
9+
Architecture: any
10+
Depends: ${shlibs:Depends}, ${misc:Depends}, libgtk-3-0, libayatana-appindicator3-1
11+
Description: An open source alternative to Raycast, and in rust

debian/rules

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/make -f
2+
3+
export PATH := $(HOME)/.cargo/bin:$(PATH)
4+
5+
%:
6+
dh $@
7+
8+
override_dh_auto_build:
9+
cargo build --release --target x86_64-unknown-linux-gnu
10+
11+
override_dh_auto_install:
12+
install -d $(CURDIR)/debian/rustcast/usr/bin
13+
install -m 755 target/x86_64-unknown-linux-gnu/release/rustcast $(CURDIR)/debian/rustcast/usr/bin/rustcast

rust-toolchain.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[toolchain]
22
channel = "stable"
33
components = ["rustfmt", "clippy", "cargo"]
4-
targets = ["x86_64-apple-darwin", "x86_64-pc-windows-msvc"]
4+
targets = ["x86_64-apple-darwin", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu"]
55
profile = "default"

src/app/tile/elm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ pub fn new(
7676
#[cfg(not(target_os = "linux"))] hotkey: HotKey,
7777
config: &Config,
7878
) -> (Tile, Task<Message>) {
79+
tracing::trace!(target: "elm_init", "Initing ELM");
80+
7981
#[allow(unused_mut)]
8082
let mut settings = default_settings();
8183

@@ -89,6 +91,8 @@ pub fn new(
8991
settings.position = Position::Specific(pos);
9092
}
9193

94+
tracing::trace!(target: "elm_init", "Opening window");
95+
9296
// id unused on windows, but not macos
9397
#[allow(unused)]
9498
let (id, open) = window::open(settings);

src/app/tile/update.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::utils::index_installed_apps;
2323

2424
#[allow(clippy::too_many_lines)]
2525
pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
26-
tracing::trace!("Handling update (message: {:?})", message);
26+
tracing::trace!(target: "update", "{:?}", message);
2727

2828
match message {
2929
Message::OpenWindow => {

src/app_finding/linux.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use std::{fs, path::Path};
2+
3+
use freedesktop_desktop_entry::DesktopEntry;
4+
use glob::glob;
5+
use iced::widget::image::Handle;
6+
use image::{ImageReader, RgbaImage};
7+
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
8+
9+
use crate::{
10+
app::{
11+
apps::{App, AppCommand, AppData},
12+
tile::elm::default_app_paths,
13+
},
14+
config::Config,
15+
};
16+
17+
pub fn get_installed_linux_apps(config: &Config) -> Vec<App> {
18+
let paths = default_app_paths();
19+
let store_icons = config.theme.show_icons;
20+
21+
let apps: Vec<App> = paths
22+
.par_iter()
23+
.map(|path| {
24+
let mut pattern = path.clone();
25+
if !pattern.ends_with('/') {
26+
pattern.push('/');
27+
}
28+
pattern.push_str("**/*.desktop");
29+
30+
get_installed_apps_glob(&pattern, store_icons)
31+
})
32+
.flatten()
33+
.collect();
34+
35+
apps
36+
}
37+
38+
fn get_installed_apps_glob(pattern: &str, store_icons: bool) -> Vec<App> {
39+
glob(pattern)
40+
.unwrap()
41+
.flatten()
42+
.flat_map(|entry| get_installed_apps(entry.as_path(), store_icons))
43+
.collect()
44+
}
45+
46+
fn get_installed_apps(path: &Path, store_icons: bool) -> Vec<App> {
47+
let mut apps = Vec::new();
48+
49+
let Ok(content) = fs::read_to_string(path) else {
50+
return apps;
51+
};
52+
53+
let Ok(de) = DesktopEntry::from_str(path, &content, None::<&[String]>) else {
54+
return apps;
55+
};
56+
57+
if de.no_display() || de.hidden() {
58+
return apps;
59+
}
60+
61+
let Some(name) = de.desktop_entry("Name") else {
62+
return apps;
63+
};
64+
let desc = de.desktop_entry("Comment").unwrap_or("");
65+
let Some(exec) = de.exec() else {
66+
return apps;
67+
};
68+
69+
let exec = exec.to_string();
70+
let mut parts = exec.split_whitespace().filter(|p| !p.starts_with("%"));
71+
72+
let Some(cmd) = parts.next() else {
73+
return apps;
74+
};
75+
76+
let args = parts.map(str::to_owned).collect::<Vec<_>>().join(" ");
77+
78+
let icon = if store_icons {
79+
de.icon()
80+
.map(str::to_owned)
81+
.and_then(|icon_name| find_icon_handle(&icon_name))
82+
} else {
83+
None
84+
};
85+
86+
apps.push(App::new(
87+
&name,
88+
&name.to_lowercase(),
89+
&desc,
90+
AppData::Command {
91+
command: cmd.to_string(),
92+
alias: args,
93+
icon,
94+
},
95+
));
96+
97+
apps
98+
}
99+
100+
pub fn handle_from_png(path: &Path) -> Option<Handle> {
101+
let img = ImageReader::open(path).ok()?.decode().ok()?.to_rgba8();
102+
let image = RgbaImage::from_raw(img.width(), img.height(), img.to_vec())?;
103+
Some(Handle::from_rgba(
104+
image.width(),
105+
image.height(),
106+
image.into_raw(),
107+
))
108+
}
109+
110+
fn find_icon_handle(name: &str) -> Option<Handle> {
111+
let paths = default_app_paths();
112+
113+
for dir in paths {
114+
let mut pattern = dir.clone();
115+
116+
if !pattern.ends_with('/') {
117+
pattern.push('/');
118+
}
119+
pattern.push_str(&format!("icons/**/{}*", name));
120+
121+
for entry in glob(&pattern).ok()?.flatten() {
122+
if let Some(handle) = handle_from_png(&entry) {
123+
return Some(handle);
124+
}
125+
}
126+
}
127+
128+
None
129+
}

src/app_finding/macos.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use std::path::PathBuf;
2+
use std::process::exit;
3+
use crate::{app::apps::App, config::Config, utils::index_installed_apps};
4+
5+
6+
fn get_installed_apps(dir: impl AsRef<Path>, store_icons: bool) -> Vec<App> {
7+
let entries: Vec<_> = fs::read_dir(dir.as_ref())
8+
.unwrap_or_else(|x| {
9+
tracing::error!(
10+
"An error occurred while reading dir ({}) {}",
11+
dir.as_ref().to_str().unwrap_or(""),
12+
x
13+
);
14+
exit(-1)
15+
})
16+
.filter_map(|x| x.ok())
17+
.collect();
18+
19+
entries
20+
.into_par_iter()
21+
.filter_map(|x| {
22+
let file_type = x.file_type().unwrap_or_else(|e| {
23+
tracing::error!("Failed to get file type: {}", e.to_string());
24+
exit(-1)
25+
});
26+
if !file_type.is_dir() {
27+
return None;
28+
}
29+
30+
let file_name_os = x.file_name();
31+
let file_name = file_name_os.into_string().unwrap_or_else(|e| {
32+
tracing::error!("Failed to to get file_name_os: {}", e.to_string_lossy());
33+
exit(-1)
34+
});
35+
if !file_name.ends_with(".app") {
36+
return None;
37+
}
38+
39+
let path = x.path();
40+
let path_str = path.to_str().map(|x| x.to_string()).unwrap_or_else(|| {
41+
tracing::error!("Unable to get file_name");
42+
exit(-1)
43+
});
44+
45+
let icons = if store_icons {
46+
match fs::read_to_string(format!("{}/Contents/Info.plist", path_str)).map(
47+
|content| {
48+
let icon_line = content
49+
.lines()
50+
.scan(false, |expect_next, line| {
51+
if *expect_next {
52+
*expect_next = false;
53+
// Return this line to the iterator
54+
return Some(Some(line));
55+
}
56+
57+
if line.trim() == "<key>CFBundleIconFile</key>" {
58+
*expect_next = true;
59+
}
60+
61+
// For lines that are not the one after the key, return None to skip
62+
Some(None)
63+
})
64+
.flatten() // remove the Nones
65+
.next()
66+
.map(|x| {
67+
x.trim()
68+
.strip_prefix("<string>")
69+
.unwrap_or("")
70+
.strip_suffix("</string>")
71+
.unwrap_or("")
72+
});
73+
74+
handle_from_icns(Path::new(&format!(
75+
"{}/Contents/Resources/{}",
76+
path_str,
77+
icon_line.unwrap_or("AppIcon.icns")
78+
)))
79+
},
80+
) {
81+
Ok(Some(a)) => Some(a),
82+
_ => {
83+
// Fallback method
84+
let direntry = fs::read_dir(format!("{}/Contents/Resources", path_str))
85+
.into_iter()
86+
.flatten()
87+
.filter_map(|x| {
88+
let file = x.ok()?;
89+
let name = file.file_name();
90+
let file_name = name.to_str()?;
91+
if file_name.ends_with(".icns") {
92+
Some(file.path())
93+
} else {
94+
None
95+
}
96+
})
97+
.collect::<Vec<PathBuf>>();
98+
99+
if direntry.len() > 1 {
100+
let icns_vec = direntry
101+
.iter()
102+
.filter(|x| x.ends_with("AppIcon.icns"))
103+
.collect::<Vec<&PathBuf>>();
104+
handle_from_icns(icns_vec.first().unwrap_or(&&PathBuf::new()))
105+
} else if !direntry.is_empty() {
106+
handle_from_icns(direntry.first().unwrap_or(&PathBuf::new()))
107+
} else {
108+
None
109+
}
110+
}
111+
}
112+
} else {
113+
None
114+
};
115+
116+
let name = file_name.strip_suffix(".app").unwrap().to_string();
117+
Some(App::new_executable(
118+
&name,
119+
&name.to_lowercase(),
120+
"Application",
121+
path,
122+
icons,
123+
))
124+
})
125+
.collect()
126+
}
127+
128+
pub fn get_installed_macos_apps(config: &Config) -> anyhow::Result<Vec<App>> {
129+
let store_icons = config.theme.show_icons;
130+
let user_local_path = std::env::var("HOME").unwrap() + "/Applications/";
131+
let paths: Vec<String> = vec![
132+
"/Applications/".to_string(),
133+
user_local_path.to_string(),
134+
"/System/Applications/".to_string(),
135+
"/System/Applications/Utilities/".to_string(),
136+
];
137+
138+
let mut apps = index_installed_apps(config)?;
139+
apps.par_extend(
140+
paths
141+
.par_iter()
142+
.map(|path| get_installed_apps(path, store_icons))
143+
.flatten(),
144+
);
145+
146+
Ok(apps)
147+
}

src/app_finding/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#[cfg(target_os = "linux")]
2+
mod linux;
3+
#[cfg(target_os = "macos")]
4+
mod macos;
5+
#[cfg(target_os = "windows")]
6+
mod windows;

0 commit comments

Comments
 (0)