feat(command-palette): add action bar to command palette

This commit introduces functionality to manage hidden items in the frecency store, including fetching hidden item IDs, hiding items, and deleting frecency entries. Additionally, the apps store is updated to filter out hidden applications based on the new frecency store logic.
This commit is contained in:
ByteAtATime 2025-06-28 14:44:05 -07:00
parent 7c14ea6c15
commit 398ed86c40
No known key found for this signature in database
7 changed files with 247 additions and 10 deletions

View file

@ -40,6 +40,10 @@ impl FrecencyManager {
)",
[],
)?;
db.execute(
"CREATE TABLE IF NOT EXISTS hidden_items (item_id TEXT PRIMARY KEY)",
[],
)?;
Ok(())
}
@ -71,4 +75,29 @@ impl FrecencyManager {
.collect::<RusqliteResult<Vec<_>>>()
.map_err(|e| e.into())
}
pub fn delete_frecency_entry(&self, item_id: String) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
db.execute("DELETE FROM frecency WHERE item_id = ?", params![item_id])?;
Ok(())
}
pub fn hide_item(&self, item_id: String) -> Result<(), AppError> {
let db = self.db.lock().unwrap();
db.execute(
"INSERT OR IGNORE INTO hidden_items (item_id) VALUES (?)",
params![item_id],
)?;
Ok(())
}
pub fn get_hidden_item_ids(&self) -> Result<Vec<String>, AppError> {
let db = self.db.lock().unwrap();
let mut stmt = db.prepare("SELECT item_id FROM hidden_items")?;
let ids_iter = stmt.query_map([], |row| row.get(0))?;
ids_iter
.collect::<RusqliteResult<Vec<String>>>()
.map_err(|e| e.into())
}
}

View file

@ -15,7 +15,6 @@ mod quicklinks;
mod snippets;
mod system;
use crate::ai::{ai_ask_stream, AskOptions};
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
use crate::{app::App, cache::AppCache};
use ai::AiUsageManager;
@ -119,6 +118,27 @@ fn get_frecency_data(app: tauri::AppHandle) -> Result<Vec<frecency::FrecencyData
.map_err(|e| e.to_string())
}
#[tauri::command]
fn delete_frecency_entry(app: tauri::AppHandle, item_id: String) -> Result<(), String> {
app.state::<FrecencyManager>()
.delete_frecency_entry(item_id)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn hide_item(app: tauri::AppHandle, item_id: String) -> Result<(), String> {
app.state::<FrecencyManager>()
.hide_item(item_id)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn get_hidden_item_ids(app: tauri::AppHandle) -> Result<Vec<String>, String> {
app.state::<FrecencyManager>()
.get_hidden_item_ids()
.map_err(|e| e.to_string())
}
fn setup_background_refresh() {
thread::spawn(|| {
thread::sleep(Duration::from_secs(60));
@ -243,6 +263,9 @@ pub fn run() {
system::trash,
record_usage,
get_frecency_data,
delete_frecency_entry,
hide_item,
get_hidden_item_ids,
snippets::create_snippet,
snippets::list_snippets,
snippets::update_snippet,

View file

@ -1,10 +1,12 @@
import { invoke } from '@tauri-apps/api/core';
import { frecencyStore } from './frecency.svelte';
type App = { name: string; comment?: string; exec: string; icon_path?: string };
class AppsStore {
apps = $state<App[]>([]);
rawApps = $state<App[]>([]);
isLoading = $state(true);
apps = $derived(this.rawApps.filter((app) => !frecencyStore.hiddenItemIds.includes(app.exec)));
constructor() {
this.fetchApps();
@ -13,10 +15,10 @@ class AppsStore {
async fetchApps() {
this.isLoading = true;
try {
this.apps = await invoke<App[]>('get_installed_apps');
this.rawApps = await invoke<App[]>('get_installed_apps');
} catch (e) {
console.error('Failed to fetch installed apps:', e);
this.apps = [];
this.rawApps = [];
} finally {
this.isLoading = false;
}

View file

@ -13,6 +13,14 @@
import { quicklinksStore, type Quicklink } from '$lib/quicklinks.svelte';
import { appsStore } from '$lib/apps.svelte';
import { frecencyStore } from '$lib/frecency.svelte';
import ActionBar from './nodes/shared/ActionBar.svelte';
import { Button } from './ui/button';
import { Kbd } from './ui/kbd';
import ActionMenu from './nodes/shared/ActionMenu.svelte';
import * as DropdownMenu from './ui/dropdown-menu';
import { Separator } from './ui/separator';
import { shortcutToText } from '$lib/renderKey';
import { viewManager } from '$lib/viewManager.svelte';
type Props = {
plugins: PluginInfo[];
@ -131,10 +139,12 @@
return items;
});
const selectedItem = $derived(displayItems[selectedIndex]);
$effect(() => {
const selectedItem = displayItems[selectedIndex];
if (selectedItem?.type === 'quicklink' && selectedItem.data.link.includes('{argument}')) {
selectedQuicklinkForArgument = selectedItem.data;
const item = displayItems[selectedIndex];
if (item?.type === 'quicklink' && item.data.link.includes('{argument}')) {
selectedQuicklinkForArgument = item.data;
} else {
selectedQuicklinkForArgument = null;
}
@ -159,8 +169,10 @@
resetState();
}
async function handleEnter(item: UnifiedItem) {
frecencyStore.recordUsage(item.id);
async function handleEnter() {
if (!selectedItem) return;
const item = selectedItem;
await frecencyStore.recordUsage(item.id);
switch (item.type) {
case 'calculator':
@ -199,8 +211,74 @@
searchInputEl?.focus();
}
}
async function handleResetRanking() {
if (selectedItem) {
const itemToReset = selectedItem;
await frecencyStore.deleteEntry(itemToReset.id);
}
}
function handleCopyDeeplink() {
if (selectedItem?.type !== 'plugin') return;
const plugin = selectedItem.data as PluginInfo;
const authorOrOwner =
plugin.owner === 'raycast'
? 'raycast'
: typeof plugin.author === 'string'
? plugin.author
: (plugin.author?.name ?? 'unknown');
const deeplink = `raycast://extensions/${authorOrOwner}/${plugin.pluginName}/${plugin.commandName}`;
writeText(deeplink);
}
function handleConfigureCommand() {
if (selectedItem?.type !== 'plugin') return;
const plugin = selectedItem.data as PluginInfo;
viewManager.showSettings(plugin.pluginName);
}
function handleCopyAppName() {
if (selectedItem?.type !== 'app') return;
writeText(selectedItem.data.name);
}
function handleCopyAppPath() {
if (selectedItem?.type !== 'app') return;
writeText(selectedItem.data.exec);
}
async function handleHideApp() {
if (selectedItem?.type !== 'app') return;
const itemToHide = selectedItem;
await frecencyStore.hideItem(itemToHide.id);
}
async function handleKeyDown(e: KeyboardEvent) {
if (!selectedItem) return;
const keyMap: Record<string, (() => void) | (() => Promise<void>) | undefined> = {
'C-S-c': selectedItem.type === 'plugin' ? handleCopyDeeplink : undefined,
'C-S-,': selectedItem.type === 'plugin' ? handleConfigureCommand : undefined,
'C-.': selectedItem.type === 'app' ? handleCopyAppName : undefined,
'C-S-.': selectedItem.type === 'app' ? handleCopyAppPath : undefined,
'C-h': selectedItem.type === 'app' ? handleHideApp : undefined
};
const shortcut = `${e.metaKey ? 'M-' : ''}${e.ctrlKey ? 'C-' : ''}${e.shiftKey ? 'S-' : ''}${e.key.toLowerCase()}`;
const action =
keyMap[shortcut.replace('meta', 'M').replace('control', 'C').replace('shift', 'S')];
if (action) {
e.preventDefault();
await action();
}
}
</script>
<svelte:window onkeydown={handleKeyDown} />
<main class="bg-background text-foreground flex h-screen flex-col">
<header class="flex h-12 shrink-0 items-center border-b px-2">
<div class="relative flex w-full items-center">
@ -305,4 +383,64 @@
{/snippet}
</BaseList>
</div>
{#if selectedItem}
<ActionBar>
{#snippet primaryAction({ props })}
{@const primaryActionText =
selectedItem.type === 'app'
? 'Open Application'
: selectedItem.type === 'quicklink'
? 'Open Quicklink'
: 'Open Command'}
<Button {...props} onclick={handleEnter}>
{primaryActionText}
<Kbd></Kbd>
</Button>
{/snippet}
{#snippet actions()}
<ActionMenu>
{#if selectedItem.type === 'plugin'}
<DropdownMenu.Item onclick={handleResetRanking}>Reset Ranking</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleCopyDeeplink}>
Copy Deeplink
<DropdownMenu.Shortcut>
{shortcutToText({ key: 'c', modifiers: ['ctrl', 'shift'] })}
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleConfigureCommand}>
Configure Command
<DropdownMenu.Shortcut>
{shortcutToText({ key: ',', modifiers: ['ctrl', 'shift'] })}
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
{:else if selectedItem.type === 'app'}
<DropdownMenu.Item onclick={handleResetRanking}>Reset Ranking</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleCopyAppName}>
Copy Name
<DropdownMenu.Shortcut>
{shortcutToText({ key: '.', modifiers: ['ctrl'] })}
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleCopyAppPath}>
Copy Path
<DropdownMenu.Shortcut>
{shortcutToText({ key: '.', modifiers: ['ctrl', 'shift'] })}
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleHideApp}>
Hide Application
<DropdownMenu.Shortcut>
{shortcutToText({ key: 'h', modifiers: ['ctrl'] })}
</DropdownMenu.Shortcut>
</DropdownMenu.Item>
{/if}
</ActionMenu>
{/snippet}
</ActionBar>
{/if}
</main>

View file

@ -12,6 +12,7 @@
import PasswordInput from './PasswordInput.svelte';
import * as Tabs from '$lib/components/ui/tabs';
import AiSettingsView from './AiSettingsView.svelte';
import { viewManager } from '$lib/viewManager.svelte';
type Props = {
plugins: PluginInfo[];
@ -32,6 +33,8 @@
let { plugins, onBack, onSavePreferences, onGetPreferences, currentPreferences }: Props =
$props();
const { pluginToSelectInSettings } = $derived(viewManager);
let selectedIndex = $state(0);
let preferenceValues = $state<Record<string, unknown>>({});
let searchText = $state('');
@ -120,6 +123,17 @@
}
});
$effect(() => {
if (pluginToSelectInSettings) {
const index = displayItems.findIndex(
(item) => item.type === 'extension' && item.data.pluginName === pluginToSelectInSettings
);
if (index > -1) {
selectedIndex = index;
}
}
});
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();

View file

@ -9,9 +9,11 @@ type FrecencyDataItem = {
class FrecencyStore {
data = $state<FrecencyDataItem[]>([]);
isLoading = $state(true);
hiddenItemIds = $state<string[]>([]);
constructor() {
this.fetchData();
this.fetchHiddenItems();
}
async fetchData() {
@ -26,6 +28,14 @@ class FrecencyStore {
}
}
async fetchHiddenItems() {
try {
this.hiddenItemIds = await invoke<string[]>('get_hidden_item_ids');
} catch (e) {
console.error('Failed to fetch hidden items:', e);
}
}
async recordUsage(itemId: string) {
try {
await invoke('record_usage', { itemId });
@ -34,6 +44,24 @@ class FrecencyStore {
console.error(`Failed to record usage for ${itemId}:`, e);
}
}
async deleteEntry(itemId: string) {
try {
await invoke('delete_frecency_entry', { itemId });
await this.fetchData();
} catch (e) {
console.error(`Failed to delete frecency entry for ${itemId}:`, e);
}
}
async hideItem(itemId: string) {
try {
await invoke('hide_item', { itemId });
await this.fetchHiddenItems();
} catch (e) {
console.error(`Failed to hide item ${itemId}:`, e);
}
}
}
export const frecencyStore = new FrecencyStore();

View file

@ -28,6 +28,7 @@ class ViewManager {
quicklinkToEdit = $state<Quicklink | undefined>(undefined);
snippetsForImport = $state<any[] | null>(null);
commandToConfirm = $state<PluginInfo | null>(null);
pluginToSelectInSettings = $state<string | undefined>(undefined);
oauthState: OauthState = $state(null);
oauthStatus: 'initial' | 'authorizing' | 'success' | 'error' = $state('initial');
@ -37,10 +38,12 @@ class ViewManager {
uiStore.setCurrentRunningPlugin(null);
this.snippetsForImport = null;
this.commandToConfirm = null;
this.pluginToSelectInSettings = undefined;
};
showSettings = () => {
showSettings = (pluginName?: string) => {
this.currentView = 'settings';
this.pluginToSelectInSettings = pluginName;
};
showExtensions = () => {