use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::sync::mpsc;
use std::thread;
use std::time::Instant;
use eframe::egui;
use global_hotkey::{
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
hotkey::{Code, HotKey, Modifiers},
};
use windows_sys::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN};
use wintheon::file::{FileEntry, FileIcon, IconSize, Priority};
use wintheon::gather::{Gatherer, Origin, WeightedEntry, WeightedEntryIteratorExt};
const ICON: IconSize = IconSize::Custom(64);
const ICON_PX: f32 = ICON.pixels() as f32 / 2.0;
const WIN_W: f32 = 580.0;
const WIN_H_COMPACT: f32 = 120.0;
const WIN_H: f32 = 720.0;
const MAX_CARD_WIDTH: f32 = 540.0;
const WIN_TOP_OFFSET: f32 = 120.0;
mod icon_cache {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::UNIX_EPOCH;
use mmap_io::mmap::MemoryMappedFile;
const HEADER_LEN: u64 = 8;
static CACHE_DIR: OnceLock<PathBuf> = OnceLock::new();
fn cache_dir() -> &'static Path {
CACHE_DIR.get_or_init(|| {
let dir = std::env::temp_dir().join("wintheon-launcher-icons");
let _ = std::fs::create_dir_all(&dir);
dir
})
}
fn cache_path(source: &Path, size: u32) -> PathBuf {
let mut h = DefaultHasher::new();
source.to_string_lossy().hash(&mut h);
cache_dir().join(format!("{:016x}_{size}.rgba", h.finish()))
}
fn rgba_byte_len(size: u32) -> u64 {
(size as u64) * (size as u64) * 4
}
fn source_stamp(source: &Path) -> Option<u64> {
let mtime = std::fs::metadata(source).ok()?.modified().ok()?;
Some(mtime.duration_since(UNIX_EPOCH).ok()?.as_nanos() as u64)
}
fn read_header(path: &Path) -> Option<u64> {
let mut f = std::fs::File::open(path).ok()?;
let mut buf = [0u8; HEADER_LEN as usize];
f.read_exact(&mut buf).ok()?;
Some(u64::from_le_bytes(buf))
}
pub fn try_load(source: &Path, size: u32) -> Option<Vec<u8>> {
let path = cache_path(source, size);
let mmap = MemoryMappedFile::open_ro(&path).ok()?;
if mmap.len() != HEADER_LEN + rgba_byte_len(size) {
return None;
}
let header: [u8; HEADER_LEN as usize] =
mmap.as_slice(0, HEADER_LEN).ok()?.try_into().ok()?;
if u64::from_le_bytes(header) != source_stamp(source)? {
return None;
}
Some(
mmap.as_slice(HEADER_LEN, rgba_byte_len(size))
.ok()?
.to_vec(),
)
}
pub fn is_cached(source: &Path, size: u32) -> bool {
let path = cache_path(source, size);
let Ok(meta) = std::fs::metadata(&path) else {
return false;
};
if meta.len() != HEADER_LEN + rgba_byte_len(size) {
return false;
}
let (Some(stored), Some(current)) = (read_header(&path), source_stamp(source)) else {
return false;
};
stored == current
}
pub fn store(source: &Path, size: u32, rgba: &[u8]) {
let Some(stamp) = source_stamp(source) else {
return;
};
let path = cache_path(source, size);
let mut buf = Vec::with_capacity(HEADER_LEN as usize + rgba.len());
buf.extend_from_slice(&stamp.to_le_bytes());
buf.extend_from_slice(rgba);
let _ = std::fs::write(path, buf);
}
}
fn launcher_position() -> egui::Pos2 {
let screen_w = unsafe { GetSystemMetrics(SM_CXSCREEN) as f32 };
egui::Pos2::new(((screen_w - WIN_W) / 2.0).max(0.0), WIN_TOP_OFFSET)
}
fn main() -> eframe::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_writer(std::io::stderr)
.init();
let entries = collect_entries();
let prewarm: Vec<(PathBuf, FileIcon)> = entries
.iter()
.filter_map(|e| {
let icon = e.weighted.entry.icon().ok()?;
Some((e.launch_target().to_path_buf(), icon))
})
.collect();
spawn_prewarm_worker(prewarm);
let hotkey_manager =
GlobalHotKeyManager::new().expect("failed to initialize the global-hotkey manager");
let hotkey = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::Space);
hotkey_manager
.register(hotkey)
.expect("failed to register Shift+Alt+Space (already taken?)");
let hotkey_id = hotkey.id();
let (hotkey_tx, hotkey_rx) = mpsc::channel::<()>();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([WIN_W, WIN_H_COMPACT])
.with_position(launcher_position())
.with_decorations(false)
.with_always_on_top()
.with_resizable(false),
..Default::default()
};
eframe::run_native(
"wintheon example launcher",
options,
Box::new(move |cc| {
cc.egui_ctx.global_style_mut(|style| {
style.spacing.item_spacing = egui::vec2(8.0, 6.0);
});
let egui_ctx = cc.egui_ctx.clone();
thread::spawn(move || {
let receiver = GlobalHotKeyEvent::receiver();
while let Ok(event) = receiver.recv() {
if event.id != hotkey_id || event.state != HotKeyState::Pressed {
continue;
}
if hotkey_tx.send(()).is_err() {
break;
}
egui_ctx.request_repaint();
}
});
Ok(Box::new(Launcher::new(entries, hotkey_rx, hotkey_manager)))
}),
)
}
fn collect_entries() -> Vec<LauncherEntry> {
let gatherer = Gatherer::new()
.with_desktop(Priority(1.0))
.with_start_menu(Priority(1.5))
.with_windows_apps(Priority(2.0));
gatherer
.scan()
.filter_map(|r| r.ok())
.enumerate()
.map(|(i, w)| LauncherEntry::new(i, w))
.collect()
}
#[derive(Default, Clone)]
struct EntryMeta {
company: Option<String>,
file_version: Option<String>,
product_version: Option<String>,
original_filename: Option<String>,
copyright: Option<String>,
}
impl EntryMeta {
fn from_entry(entry: &dyn FileEntry) -> Self {
entry
.version_info()
.ok()
.and_then(|info| info.english().cloned())
.map(|fi| Self {
company: fi.company_name,
file_version: fi.file_version,
product_version: fi.product_version,
original_filename: fi.original_filename,
copyright: fi.legal_copyright,
})
.unwrap_or_default()
}
}
fn spawn_prewarm_worker(work: Vec<(PathBuf, FileIcon)>) {
thread::spawn(move || {
let size = ICON.pixels();
let total = work.len();
let started = Instant::now();
let mut extracted = 0usize;
let mut already_cached = 0usize;
let mut failed = 0usize;
for (cache_key, icon) in work {
if icon_cache::is_cached(&cache_key, size) {
already_cached += 1;
continue;
}
match icon.extract_icon_at(ICON) {
Some(rgba) => {
icon_cache::store(&cache_key, size, &rgba);
extracted += 1;
}
None => failed += 1,
}
}
tracing::info!(
total,
extracted,
already_cached,
failed,
elapsed_ms = started.elapsed().as_millis() as u64,
"icon prewarm complete"
);
});
}
#[derive(Default)]
enum TextureSlot {
#[default]
Unloaded,
Failed,
Loaded(egui::TextureHandle),
}
struct LauncherEntry {
idx: usize,
weighted: WeightedEntry,
metadata: OnceLock<EntryMeta>,
texture: RefCell<TextureSlot>,
}
impl AsRef<WeightedEntry> for LauncherEntry {
fn as_ref(&self) -> &WeightedEntry {
&self.weighted
}
}
impl LauncherEntry {
fn new(idx: usize, weighted: WeightedEntry) -> Self {
Self {
idx,
weighted,
metadata: OnceLock::new(),
texture: RefCell::new(TextureSlot::Unloaded),
}
}
fn metadata(&self) -> &EntryMeta {
self.metadata
.get_or_init(|| EntryMeta::from_entry(self.weighted.entry.as_ref()))
}
fn launch_target(&self) -> &Path {
self.weighted
.entry
.link_path()
.unwrap_or_else(|| self.weighted.entry.path())
}
fn ensure_texture(&self, ctx: &egui::Context) {
let mut slot = self.texture.borrow_mut();
if !matches!(*slot, TextureSlot::Unloaded) {
return;
}
let key = self.launch_target();
let size = ICON.pixels();
let rgba = icon_cache::try_load(key, size).or_else(|| {
let icon = self.weighted.entry.icon().ok()?;
let extracted = icon.extract_icon_at(ICON)?;
icon_cache::store(key, size, &extracted);
Some(extracted)
});
*slot = match rgba {
Some(bytes) => {
let px = ICON.pixels() as usize;
let img = egui::ColorImage::from_rgba_unmultiplied([px, px], &bytes);
let tex = ctx.load_texture(
format!("launcher_icon_{}", self.idx),
img,
egui::TextureOptions::LINEAR,
);
TextureSlot::Loaded(tex)
}
None => TextureSlot::Failed,
};
}
fn texture_handle(&self) -> Option<egui::TextureHandle> {
match &*self.texture.borrow() {
TextureSlot::Loaded(t) => Some(t.clone()),
_ => None,
}
}
}
struct Launcher {
entries: Vec<LauncherEntry>,
query: String,
selected: usize,
visible: bool,
request_focus: bool,
last_compact: bool,
hotkey_rx: mpsc::Receiver<()>,
_hotkey_manager: GlobalHotKeyManager,
}
impl Launcher {
fn new(
entries: Vec<LauncherEntry>,
hotkey_rx: mpsc::Receiver<()>,
hotkey_manager: GlobalHotKeyManager,
) -> Self {
Self {
entries,
query: String::new(),
selected: 0,
visible: true,
request_focus: true,
last_compact: true,
hotkey_rx,
_hotkey_manager: hotkey_manager,
}
}
fn show(&mut self, ctx: &egui::Context) {
self.visible = true;
self.request_focus = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
}
fn hide(&mut self, ctx: &egui::Context) {
self.visible = false;
self.query.clear();
self.selected = 0;
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(false));
}
fn toggle_visibility(&mut self, ctx: &egui::Context) {
if self.visible {
self.hide(ctx);
} else {
self.show(ctx);
}
}
fn apply_compact_state(&mut self, ctx: &egui::Context) {
let compact = self.query.is_empty();
if compact == self.last_compact {
return;
}
self.last_compact = compact;
let height = if compact { WIN_H_COMPACT } else { WIN_H };
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(WIN_W, height)));
}
}
impl eframe::App for Launcher {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
while self.hotkey_rx.try_recv().is_ok() {
self.toggle_visibility(&ctx);
}
let down = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown));
let up = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp));
let enter = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter));
let escape = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape));
if escape {
self.hide(&ctx);
return;
}
let ranked = self.compute_ranked_indices();
if !ranked.is_empty() {
if down {
self.selected = (self.selected + 1).min(ranked.len() - 1);
}
if up {
self.selected = self.selected.saturating_sub(1);
}
if self.selected >= ranked.len() {
self.selected = ranked.len() - 1;
}
} else {
self.selected = 0;
}
if enter {
if let Some(&entry_idx) = ranked.get(self.selected) {
let target = self.entries[entry_idx].launch_target().to_path_buf();
spawn(&target);
}
self.hide(&ctx);
return;
}
self.apply_compact_state(&ctx);
egui::Frame::default()
.inner_margin(egui::Margin::symmetric(20, 18))
.show(ui, |ui| self.render(ui, &ctx, &ranked));
}
}
struct Indexed<'a>(usize, &'a LauncherEntry);
impl AsRef<WeightedEntry> for Indexed<'_> {
fn as_ref(&self) -> &WeightedEntry {
&self.1.weighted
}
}
impl Launcher {
fn compute_ranked_indices(&self) -> Vec<usize> {
self.entries
.iter()
.enumerate()
.map(|(i, e)| Indexed(i, e))
.sorted_by_score(&self.query)
.into_iter()
.map(|Indexed(i, _)| i)
.collect()
}
fn render(&mut self, ui: &mut egui::Ui, ctx: &egui::Context, ranked: &[usize]) {
ui.horizontal(|ui| {
ui.heading(egui::RichText::new("wintheon").size(22.0).strong());
ui.label(
egui::RichText::new(format!("· {} entries", self.entries.len()))
.size(13.0)
.weak(),
);
});
ui.add_space(10.0);
let prev_query_len = self.query.len();
let search_resp = ui.add(
egui::TextEdit::singleline(&mut self.query)
.desired_width(f32::INFINITY)
.hint_text("Search…")
.font(egui::FontId::proportional(18.0))
.margin(egui::Margin::symmetric(10, 8)),
);
if self.request_focus {
search_resp.request_focus();
self.request_focus = false;
}
if search_resp.changed() || self.query.len() != prev_query_len {
self.selected = 0;
}
if self.query.is_empty() {
return;
}
ui.add_space(12.0);
let selected = self.selected;
let entries = &self.entries;
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
if ranked.is_empty() {
self.render_empty_state(ui);
return;
}
for (row_idx, &entry_idx) in ranked.iter().enumerate() {
render_row(row_idx, &entries[entry_idx], row_idx == selected, ui, ctx);
ui.add_space(6.0);
}
});
}
fn render_empty_state(&self, ui: &mut egui::Ui) {
ui.add_space(60.0);
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new("No matches").size(18.0).weak());
ui.add_space(4.0);
ui.label(
egui::RichText::new(format!("for {:?}", self.query))
.size(13.0)
.weak()
.italics(),
);
});
}
}
fn render_row(
row_idx: usize,
entry: &LauncherEntry,
selected: bool,
ui: &mut egui::Ui,
ctx: &egui::Context,
) {
let approx_rect =
egui::Rect::from_min_size(ui.next_widget_position(), egui::vec2(MAX_CARD_WIDTH, 100.0));
if selected || ui.is_rect_visible(approx_rect) {
entry.ensure_texture(ctx);
}
let texture = entry.texture_handle();
let name = entry.weighted.entry.display_name();
let meta = entry.metadata().clone();
let origin = entry.weighted.origin.clone();
let full_path = entry.weighted.entry.path().display().to_string();
let launch_target = entry.launch_target().to_path_buf();
let row_id = ui.id().with("launcher_row").with(row_idx);
let hovered = ctx.read_response(row_id).is_some_and(|r| r.hovered());
let visuals = &ui.style().visuals;
let card_fill = if selected {
visuals.widgets.active.weak_bg_fill
} else if hovered {
visuals.widgets.hovered.weak_bg_fill
} else {
visuals.faint_bg_color
};
let frame = egui::Frame::default()
.fill(card_fill)
.inner_margin(egui::Margin::symmetric(14, 12))
.corner_radius(10.0);
let frame_response = frame
.show(ui, |ui| {
let card_width = ui.available_width().min(MAX_CARD_WIDTH);
ui.set_min_width(card_width);
ui.set_max_width(card_width);
ui.horizontal(|ui| {
ui.set_min_height(ICON_PX);
render_icon_slot(ui, texture.as_ref());
ui.add_space(12.0);
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
egui::RichText::new(&name)
.size(16.0)
.strong()
.color(ui.style().visuals.strong_text_color()),
);
ui.add_space(8.0);
origin_chip(ui, &origin);
});
let mut tail: Vec<String> = Vec::new();
if let Some(c) = &meta.company {
tail.push(c.clone());
}
if let Some(v) = meta
.file_version
.as_deref()
.or(meta.product_version.as_deref())
{
tail.push(format!("v{v}"));
}
if let Some(o) = &meta.original_filename {
tail.push(o.clone());
}
if !tail.is_empty() {
ui.label(egui::RichText::new(tail.join(" · ")).size(12.0).weak());
}
if let Some(cr) = meta.copyright.as_deref() {
ui.label(egui::RichText::new(cr).size(11.0).weak().italics());
}
ui.label(
egui::RichText::new(&full_path)
.size(11.0)
.monospace()
.color(ui.style().visuals.weak_text_color()),
);
});
});
})
.response;
let click = ui
.interact(frame_response.rect, row_id, egui::Sense::click())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if click.clicked() {
spawn(&launch_target);
}
if selected {
click.scroll_to_me(Some(egui::Align::Center));
}
}
fn render_icon_slot(ui: &mut egui::Ui, texture: Option<&egui::TextureHandle>) {
let size = egui::vec2(ICON_PX, ICON_PX);
match texture {
Some(tex) => {
ui.image((tex.id(), size));
}
None => {
ui.allocate_space(size);
}
}
}
fn origin_chip(ui: &mut egui::Ui, origin: &Origin) {
let (bg, label): (egui::Color32, &str) = match origin {
Origin::Desktop => (egui::Color32::from_rgb(60, 110, 200), "Desktop"),
Origin::StartMenu => (egui::Color32::from_rgb(60, 150, 90), "Start Menu"),
Origin::WindowsApps => (egui::Color32::from_rgb(150, 90, 200), "Windows Apps"),
Origin::Custom(label) => (egui::Color32::DARK_GRAY, label.as_ref()),
_ => (egui::Color32::DARK_GRAY, "?"),
};
egui::Frame::default()
.fill(bg)
.corner_radius(10.0)
.inner_margin(egui::Margin::symmetric(8, 2))
.show(ui, |ui| {
ui.label(
egui::RichText::new(label)
.size(11.0)
.strong()
.color(egui::Color32::WHITE),
);
});
}
fn spawn(path: &Path) {
let _ = Command::new("cmd")
.args(["/C", "start", "", &path.to_string_lossy()])
.spawn();
}