mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 03:07:23 +00:00
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:
parent
7c14ea6c15
commit
398ed86c40
7 changed files with 247 additions and 10 deletions
|
@ -40,6 +40,10 @@ impl FrecencyManager {
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
db.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS hidden_items (item_id TEXT PRIMARY KEY)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,4 +75,29 @@ impl FrecencyManager {
|
||||||
.collect::<RusqliteResult<Vec<_>>>()
|
.collect::<RusqliteResult<Vec<_>>>()
|
||||||
.map_err(|e| e.into())
|
.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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ mod quicklinks;
|
||||||
mod snippets;
|
mod snippets;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
use crate::ai::{ai_ask_stream, AskOptions};
|
|
||||||
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
|
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
|
||||||
use crate::{app::App, cache::AppCache};
|
use crate::{app::App, cache::AppCache};
|
||||||
use ai::AiUsageManager;
|
use ai::AiUsageManager;
|
||||||
|
@ -119,6 +118,27 @@ fn get_frecency_data(app: tauri::AppHandle) -> Result<Vec<frecency::FrecencyData
|
||||||
.map_err(|e| e.to_string())
|
.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() {
|
fn setup_background_refresh() {
|
||||||
thread::spawn(|| {
|
thread::spawn(|| {
|
||||||
thread::sleep(Duration::from_secs(60));
|
thread::sleep(Duration::from_secs(60));
|
||||||
|
@ -243,6 +263,9 @@ pub fn run() {
|
||||||
system::trash,
|
system::trash,
|
||||||
record_usage,
|
record_usage,
|
||||||
get_frecency_data,
|
get_frecency_data,
|
||||||
|
delete_frecency_entry,
|
||||||
|
hide_item,
|
||||||
|
get_hidden_item_ids,
|
||||||
snippets::create_snippet,
|
snippets::create_snippet,
|
||||||
snippets::list_snippets,
|
snippets::list_snippets,
|
||||||
snippets::update_snippet,
|
snippets::update_snippet,
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { frecencyStore } from './frecency.svelte';
|
||||||
|
|
||||||
type App = { name: string; comment?: string; exec: string; icon_path?: string };
|
type App = { name: string; comment?: string; exec: string; icon_path?: string };
|
||||||
|
|
||||||
class AppsStore {
|
class AppsStore {
|
||||||
apps = $state<App[]>([]);
|
rawApps = $state<App[]>([]);
|
||||||
isLoading = $state(true);
|
isLoading = $state(true);
|
||||||
|
apps = $derived(this.rawApps.filter((app) => !frecencyStore.hiddenItemIds.includes(app.exec)));
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fetchApps();
|
this.fetchApps();
|
||||||
|
@ -13,10 +15,10 @@ class AppsStore {
|
||||||
async fetchApps() {
|
async fetchApps() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
try {
|
try {
|
||||||
this.apps = await invoke<App[]>('get_installed_apps');
|
this.rawApps = await invoke<App[]>('get_installed_apps');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch installed apps:', e);
|
console.error('Failed to fetch installed apps:', e);
|
||||||
this.apps = [];
|
this.rawApps = [];
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,14 @@
|
||||||
import { quicklinksStore, type Quicklink } from '$lib/quicklinks.svelte';
|
import { quicklinksStore, type Quicklink } from '$lib/quicklinks.svelte';
|
||||||
import { appsStore } from '$lib/apps.svelte';
|
import { appsStore } from '$lib/apps.svelte';
|
||||||
import { frecencyStore } from '$lib/frecency.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 = {
|
type Props = {
|
||||||
plugins: PluginInfo[];
|
plugins: PluginInfo[];
|
||||||
|
@ -131,10 +139,12 @@
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedItem = $derived(displayItems[selectedIndex]);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const selectedItem = displayItems[selectedIndex];
|
const item = displayItems[selectedIndex];
|
||||||
if (selectedItem?.type === 'quicklink' && selectedItem.data.link.includes('{argument}')) {
|
if (item?.type === 'quicklink' && item.data.link.includes('{argument}')) {
|
||||||
selectedQuicklinkForArgument = selectedItem.data;
|
selectedQuicklinkForArgument = item.data;
|
||||||
} else {
|
} else {
|
||||||
selectedQuicklinkForArgument = null;
|
selectedQuicklinkForArgument = null;
|
||||||
}
|
}
|
||||||
|
@ -159,8 +169,10 @@
|
||||||
resetState();
|
resetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnter(item: UnifiedItem) {
|
async function handleEnter() {
|
||||||
frecencyStore.recordUsage(item.id);
|
if (!selectedItem) return;
|
||||||
|
const item = selectedItem;
|
||||||
|
await frecencyStore.recordUsage(item.id);
|
||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'calculator':
|
case 'calculator':
|
||||||
|
@ -199,8 +211,74 @@
|
||||||
searchInputEl?.focus();
|
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>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeyDown} />
|
||||||
|
|
||||||
<main class="bg-background text-foreground flex h-screen flex-col">
|
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||||
<header class="flex h-12 shrink-0 items-center border-b px-2">
|
<header class="flex h-12 shrink-0 items-center border-b px-2">
|
||||||
<div class="relative flex w-full items-center">
|
<div class="relative flex w-full items-center">
|
||||||
|
@ -305,4 +383,64 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</BaseList>
|
</BaseList>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import PasswordInput from './PasswordInput.svelte';
|
import PasswordInput from './PasswordInput.svelte';
|
||||||
import * as Tabs from '$lib/components/ui/tabs';
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import AiSettingsView from './AiSettingsView.svelte';
|
import AiSettingsView from './AiSettingsView.svelte';
|
||||||
|
import { viewManager } from '$lib/viewManager.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugins: PluginInfo[];
|
plugins: PluginInfo[];
|
||||||
|
@ -32,6 +33,8 @@
|
||||||
let { plugins, onBack, onSavePreferences, onGetPreferences, currentPreferences }: Props =
|
let { plugins, onBack, onSavePreferences, onGetPreferences, currentPreferences }: Props =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
|
const { pluginToSelectInSettings } = $derived(viewManager);
|
||||||
|
|
||||||
let selectedIndex = $state(0);
|
let selectedIndex = $state(0);
|
||||||
let preferenceValues = $state<Record<string, unknown>>({});
|
let preferenceValues = $state<Record<string, unknown>>({});
|
||||||
let searchText = $state('');
|
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) {
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
@ -9,9 +9,11 @@ type FrecencyDataItem = {
|
||||||
class FrecencyStore {
|
class FrecencyStore {
|
||||||
data = $state<FrecencyDataItem[]>([]);
|
data = $state<FrecencyDataItem[]>([]);
|
||||||
isLoading = $state(true);
|
isLoading = $state(true);
|
||||||
|
hiddenItemIds = $state<string[]>([]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
|
this.fetchHiddenItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchData() {
|
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) {
|
async recordUsage(itemId: string) {
|
||||||
try {
|
try {
|
||||||
await invoke('record_usage', { itemId });
|
await invoke('record_usage', { itemId });
|
||||||
|
@ -34,6 +44,24 @@ class FrecencyStore {
|
||||||
console.error(`Failed to record usage for ${itemId}:`, e);
|
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();
|
export const frecencyStore = new FrecencyStore();
|
||||||
|
|
|
@ -28,6 +28,7 @@ class ViewManager {
|
||||||
quicklinkToEdit = $state<Quicklink | undefined>(undefined);
|
quicklinkToEdit = $state<Quicklink | undefined>(undefined);
|
||||||
snippetsForImport = $state<any[] | null>(null);
|
snippetsForImport = $state<any[] | null>(null);
|
||||||
commandToConfirm = $state<PluginInfo | null>(null);
|
commandToConfirm = $state<PluginInfo | null>(null);
|
||||||
|
pluginToSelectInSettings = $state<string | undefined>(undefined);
|
||||||
|
|
||||||
oauthState: OauthState = $state(null);
|
oauthState: OauthState = $state(null);
|
||||||
oauthStatus: 'initial' | 'authorizing' | 'success' | 'error' = $state('initial');
|
oauthStatus: 'initial' | 'authorizing' | 'success' | 'error' = $state('initial');
|
||||||
|
@ -37,10 +38,12 @@ class ViewManager {
|
||||||
uiStore.setCurrentRunningPlugin(null);
|
uiStore.setCurrentRunningPlugin(null);
|
||||||
this.snippetsForImport = null;
|
this.snippetsForImport = null;
|
||||||
this.commandToConfirm = null;
|
this.commandToConfirm = null;
|
||||||
|
this.pluginToSelectInSettings = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
showSettings = () => {
|
showSettings = (pluginName?: string) => {
|
||||||
this.currentView = 'settings';
|
this.currentView = 'settings';
|
||||||
|
this.pluginToSelectInSettings = pluginName;
|
||||||
};
|
};
|
||||||
|
|
||||||
showExtensions = () => {
|
showExtensions = () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue