feat: add hud API

This commit adds the HUD API, allowing extensions to display temporary, non-interactive messages on the screen. The implementation adds a `show_hud` Tauri command that manages a dedicated, borderless Svelte window. This window is dynamically resized to fit its content and is automatically hidden after two seconds.
This commit is contained in:
ByteAtATime 2025-06-22 09:04:10 -07:00
parent 1ae8a571cb
commit 28d1605bba
No known key found for this signature in database
8 changed files with 125 additions and 2 deletions

View file

@ -78,6 +78,15 @@ const HideToastPayloadSchema = z.object({
id: z.number()
});
const ShowHudPayloadSchema = z.object({
title: z.string()
});
const ShowHudMessageSchema = z.object({
type: z.literal('SHOW_HUD'),
payload: ShowHudPayloadSchema
});
export const CommandSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('CREATE_INSTANCE'), payload: CreateInstancePayloadSchema }),
z.object({ type: z.literal('CREATE_TEXT_INSTANCE'), payload: CreateTextInstancePayloadSchema }),
@ -292,6 +301,7 @@ const OauthRemoveTokensMessageSchema = z.object({
export const SidecarMessageWithPluginsSchema = z.union([
BatchUpdateSchema,
CommandSchema,
ShowHudMessageSchema,
LogMessageSchema,
PluginListSchema,
PreferenceValuesSchema,

8
sidecar/src/api/hud.ts Normal file
View file

@ -0,0 +1,8 @@
import { writeOutput } from '../io';
export async function showHUD(title: string): Promise<void> {
writeOutput({
type: 'SHOW_HUD',
payload: { title }
});
}

View file

@ -13,6 +13,7 @@ import { Detail } from './components/detail';
import { environment, getSelectedFinderItems, getSelectedText, open } from './environment';
import { preferencesStore } from '../preferences';
import { showToast } from './toast';
import { showHUD } from './hud';
import { BrowserExtensionAPI } from './browserExtension';
import { Clipboard } from './clipboard';
import * as OAuth from './oauth';
@ -70,6 +71,7 @@ export const getRaycastApi = () => {
getSelectedText,
open,
showToast,
showHUD,
useNavigation,
usePersistentState: <T>(
key: string,

View file

@ -0,0 +1,14 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "hud",
"description": "Capability for the HUD window",
"windows": [
"hud"
],
"permissions": [
"core:window:allow-set-size",
"core:window:allow-center",
"core:window:allow-set-min-size",
"core:window:allow-set-max-size"
]
}

View file

@ -56,6 +56,42 @@ fn get_selected_text() -> String {
get_text()
}
#[tauri::command]
async fn show_hud(app: tauri::AppHandle, title: String) -> Result<(), String> {
let url = format!("/hud?title={}", title);
let hud_window = match app.get_webview_window("hud") {
Some(window) => window,
None => tauri::WebviewWindowBuilder::new(&app, "hud", tauri::WebviewUrl::App(url.into()))
.decorations(false)
.transparent(true)
.always_on_top(true)
.skip_taskbar(true)
.center()
.min_inner_size(300.0, 80.0)
.max_inner_size(300.0, 80.0)
.inner_size(300.0, 80.0)
.build()
.map_err(|e| e.to_string())?,
};
let window_clone = hud_window.clone();
window_clone
.emit("hud-message", &title)
.map_err(|e| e.to_string())?;
window_clone
.set_ignore_cursor_events(true)
.map_err(|e| e.to_string())?;
window_clone.show().map_err(|e| e.to_string())?;
window_clone.set_focus().map_err(|e| e.to_string())?;
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let _ = window_clone.hide();
});
Ok(())
}
fn setup_background_refresh() {
thread::spawn(|| {
thread::sleep(Duration::from_secs(60));
@ -107,8 +143,8 @@ pub fn run() {
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit("deep-link", args[1].to_string());
window.show().unwrap();
window.set_focus().unwrap();
}
return;
}
@ -130,6 +166,7 @@ pub fn run() {
get_installed_apps,
launch_app,
get_selected_text,
show_hud,
filesystem::get_selected_finder_items,
extensions::install_extension,
browser_extension::browser_extension_check_connection,

View file

@ -23,6 +23,18 @@
"width": 600,
"height": 80,
"center": true
},
{
"label": "hud",
"url": "/hud",
"visible": false,
"decorations": false,
"transparent": true,
"alwaysOnTop": true,
"skipTaskbar": true,
"center": true,
"width": 300,
"height": 80
}
],
"security": {
@ -55,4 +67,4 @@
}
}
}
}
}

View file

@ -152,6 +152,11 @@ class SidecarService {
return;
}
if (typedMessage.type === 'SHOW_HUD') {
invoke('show_hud', { title: typedMessage.payload.title });
return;
}
if (typedMessage.type === 'open') {
const { target, application } = typedMessage.payload;
shellOpen(target, application).catch((err) => {

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { page } from '$app/state';
import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window';
let hudText = $derived(page.url.searchParams.get('title') ?? '');
let hudEl = $state<HTMLDivElement | null>(null);
$effect(() => {
const bounds = hudEl?.getBoundingClientRect();
if (bounds) {
const window = getCurrentWindow();
window.setMinSize(new LogicalSize(bounds.width, bounds.height));
window.setMaxSize(new LogicalSize(bounds.width, bounds.height));
window.setSize(new LogicalSize(bounds.width, bounds.height));
window.center();
}
});
</script>
<div class="flex h-screen items-center justify-center bg-transparent">
{#if hudText}
<div
class="rounded-full bg-black/70 px-4 py-2 text-sm font-medium text-white"
bind:this={hudEl}
>
{hudText}
</div>
{/if}
</div>
<style>
:global(body) {
background-color: transparent;
}
</style>