diff --git a/extensions/owl/CHANGELOG.md b/extensions/owl/CHANGELOG.md index 763a19a5d3f..536d68f5ad1 100644 --- a/extensions/owl/CHANGELOG.md +++ b/extensions/owl/CHANGELOG.md @@ -1,5 +1,11 @@ # OWL Changelog +## [Filter Keyboards] - 2026-04-23 + +- Added filter for keyboards to show only those that match the languages. +- Added default OWLs when initializing and actions to reload that configuration. +- Added actions to delete multiple owls at a time. + ## [Security Fix] - 2026-03-17 - Bump lodash/lodash-es to fix prototype pollution vulnerability (CVE-2025-13465) diff --git a/extensions/owl/package.json b/extensions/owl/package.json index 7398deb5aef..791d8ab2185 100644 --- a/extensions/owl/package.json +++ b/extensions/owl/package.json @@ -63,5 +63,8 @@ "lint": "ray lint", "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", "publish": "npx @raycast/api@latest publish" - } + }, + "platforms": [ + "macOS" + ] } diff --git a/extensions/owl/src/components/AddOWL.tsx b/extensions/owl/src/components/AddOWL.tsx index 88e625a5887..efbf47c0ca9 100644 --- a/extensions/owl/src/components/AddOWL.tsx +++ b/extensions/owl/src/components/AddOWL.tsx @@ -2,6 +2,7 @@ import { Action, ActionPanel, Form, showToast, Toast, useNavigation } from "@ray import { randomUUID } from "node:crypto"; import { useMemo, useState } from "react"; import { useKeyboards } from "../hooks/keyboards"; +import { useLanguages } from "../hooks/languages"; import { useCachedStorage } from "../hooks/storage"; import { OWLMapping } from "../types/owl"; import { StorageKey } from "../types/storage"; @@ -19,9 +20,15 @@ export function AddOWL(props: Readonly<{ base?: string }>) { const { keyboards } = useKeyboards(); const [, setOWLs] = useCachedStorage(StorageKey.OWLS, {}); + const { value: languages } = useLanguages(); + const [showAll, setShowAll] = useState(false); + const fromOptions = useMemo(() => { return keyboards .filter((keyboard) => base == null || base === "" || keyboard.includes(base)) + .filter((keyboard) => { + return showAll || languages.includes(keyboard); + }) .toSorted((a, b) => { if (a.replace(base ?? "", "").length === 0 || b.replace(base ?? "", "").length === 0) { return a.length - b.length; @@ -29,7 +36,7 @@ export function AddOWL(props: Readonly<{ base?: string }>) { return a.localeCompare(b); }); - }, [base, keyboards]); + }, [base, keyboards, showAll, languages]); const [from, setFrom] = useState(base ? (fromOptions?.[0] ?? "") : ""); const [to, setTo] = useState(""); @@ -85,23 +92,35 @@ export function AddOWL(props: Readonly<{ base?: string }>) { push(); }} /> + { + setShowAll(!showAll); + }} + /> } > - + {fromOptions.toSorted(lengthLocaleCompare).map((keyboard) => { return ; })} - - {keyboards - .filter((keyboard) => !fromOptions.includes(keyboard)) - .toSorted(lengthLocaleCompare) - .map((keyboard) => { - return ; - })} - + {showAll && ( + + {keyboards + .filter((keyboard) => !fromOptions.includes(keyboard)) + .toSorted(lengthLocaleCompare) + .map((keyboard) => { + return ; + })} + + )} @@ -109,11 +128,18 @@ export function AddOWL(props: Readonly<{ base?: string }>) { .filter((keyboard) => { return keyboard !== from; }) + .filter((keyboard) => { + return showAll || languages.includes(keyboard); + }) .toSorted(lengthLocaleCompare) .map((keyboard) => { return ; })} + + + + ); } diff --git a/extensions/owl/src/components/ClearAllOWLsAction.tsx b/extensions/owl/src/components/ClearAllOWLsAction.tsx new file mode 100644 index 00000000000..83d68b1ce16 --- /dev/null +++ b/extensions/owl/src/components/ClearAllOWLsAction.tsx @@ -0,0 +1,34 @@ +import { Action, Alert, confirmAlert, Icon } from "@raycast/api"; +import { useCachedStorage } from "../hooks/storage"; +import { OWLMapping } from "../types/owl"; +import { StorageKey } from "../types/storage"; + +export function ClearAllOWLsAction() { + const [, setOWLs] = useCachedStorage(StorageKey.OWLS, {}); + + return ( + { + if ( + await confirmAlert({ + title: "Are you sure?", + message: `Are you sure you want to clear all owls?`, + primaryAction: { + title: "Clear", + style: Alert.ActionStyle.Destructive, + }, + }) + ) { + setOWLs({}); + } + }} + /> + ); +} diff --git a/extensions/owl/src/components/DeleteAllOWLsAction.tsx b/extensions/owl/src/components/DeleteAllOWLsAction.tsx new file mode 100644 index 00000000000..d806fa36bd3 --- /dev/null +++ b/extensions/owl/src/components/DeleteAllOWLsAction.tsx @@ -0,0 +1,36 @@ +import { Action, Alert, confirmAlert, Icon, Keyboard } from "@raycast/api"; +import { useCachedStorage } from "../hooks/storage"; +import { OWLMapping } from "../types/owl"; +import { StorageKey } from "../types/storage"; + +export function DeleteAllOWLsAction(props: Readonly<{ language: string }>) { + const { language } = props; + const [, setOWLs] = useCachedStorage(StorageKey.OWLS, {}); + + return ( + { + if ( + await confirmAlert({ + title: "Are you sure?", + message: `Are you sure you want to delete all owls of ${language}?`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }) + ) { + setOWLs((previousState) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [language]: _, ...rest } = previousState; + return rest; + }); + } + }} + /> + ); +} diff --git a/extensions/owl/src/components/ResetToDefaultOWLAction.tsx b/extensions/owl/src/components/ResetToDefaultOWLAction.tsx new file mode 100644 index 00000000000..43f0faf9e1e --- /dev/null +++ b/extensions/owl/src/components/ResetToDefaultOWLAction.tsx @@ -0,0 +1,39 @@ +import { Action, Icon } from "@raycast/api"; +import { useKeyboards } from "../hooks/keyboards"; +import { useLanguages } from "../hooks/languages"; +import { useCachedStorage } from "../hooks/storage"; +import { OWLMapping } from "../types/owl"; +import { StorageKey } from "../types/storage"; +import { loadDefaultOWLs } from "../utils/loadDefaultOWLs"; + +export function ResetToDefaultOWLAction( + props: Readonly< + Omit & { + base?: string; + } + >, +) { + const { keyboards } = useKeyboards(); + const { value: languages } = useLanguages(); + const [, setOWLs] = useCachedStorage(StorageKey.OWLS, {}); + + return ( + { + await loadDefaultOWLs({ + keyboards, + languages, + setOWLs, + }); + }} + /> + ); +} diff --git a/extensions/owl/src/configure-owls.tsx b/extensions/owl/src/configure-owls.tsx index 3eb27e76a74..15ee1f94b46 100644 --- a/extensions/owl/src/configure-owls.tsx +++ b/extensions/owl/src/configure-owls.tsx @@ -1,12 +1,18 @@ import { Action, ActionPanel, Icon, List } from "@raycast/api"; import { AddOWLAction } from "./components/AddOWLAction"; +import { ClearAllOWLsAction } from "./components/ClearAllOWLsAction"; +import { DeleteAllOWLsAction } from "./components/DeleteAllOWLsAction"; +import { ResetToDefaultOWLAction } from "./components/ResetToDefaultOWLAction"; import { ViewOWLs } from "./components/ViewOWLs"; import { useLanguages } from "./hooks/languages"; import { useCachedStorage } from "./hooks/storage"; +import { useInitializeOWLs } from "./hooks/useInitializeOWLs"; import { OWLMapping } from "./types/owl"; import { StorageKey } from "./types/storage"; export default function ConfigureOWLsCommand() { + useInitializeOWLs(); + const [owls] = useCachedStorage(StorageKey.OWLS, {}); const { value: languages, isLoading } = useLanguages(); @@ -17,6 +23,7 @@ export default function ConfigureOWLsCommand() { actions={ + } /> @@ -31,6 +38,11 @@ export default function ConfigureOWLsCommand() { } /> )} + + {owls[language] !== undefined && } + + + } accessories={(owls[language] ?? []) diff --git a/extensions/owl/src/hooks/useInitializeOWLs.ts b/extensions/owl/src/hooks/useInitializeOWLs.ts new file mode 100644 index 00000000000..d3dca878c45 --- /dev/null +++ b/extensions/owl/src/hooks/useInitializeOWLs.ts @@ -0,0 +1,44 @@ +import { useEffect } from "react"; +import { OWLMapping } from "../types/owl"; +import { StorageKey } from "../types/storage"; +import { loadDefaultOWLs } from "../utils/loadDefaultOWLs"; +import { useKeyboards } from "./keyboards"; +import { useLanguages } from "./languages"; +import { useCachedStorage } from "./storage"; + +export type UseInitializeOWLs = { + isInitialized: boolean; + reinitialize: () => void; +}; + +export function useInitializeOWLs( + { showAlert = false }: { showAlert: boolean } = { showAlert: false }, +): UseInitializeOWLs { + const [, setOWLs] = useCachedStorage(StorageKey.OWLS, {}); + const [isInit, setIsInit] = useCachedStorage(StorageKey.INIT, false); + + const { keyboards } = useKeyboards(); + const { value: languages } = useLanguages(); + + useEffect(() => { + if (keyboards.length === 0 || languages.length === 0 || isInit) { + return; + } + + loadDefaultOWLs({ + keyboards, + languages, + setOWLs, + showAlert, + }).then(() => { + setIsInit(true); + }); + }, [isInit, keyboards, languages]); + + return { + isInitialized: isInit, + reinitialize: () => { + setIsInit(false); + }, + }; +} diff --git a/extensions/owl/src/owl.tsx b/extensions/owl/src/owl.tsx index 7e0e1226e9a..21927f81f53 100644 --- a/extensions/owl/src/owl.tsx +++ b/extensions/owl/src/owl.tsx @@ -17,6 +17,7 @@ import { callbackLaunchCommand, LaunchOptions } from "raycast-cross-extension"; import { useCallback, useEffect, useMemo } from "react"; import { ViewOWLAction } from "./components/ViewOWLAction"; import { useCurrentLanguage } from "./hooks/languages"; +import { useInitializeOWLs } from "./hooks/useInitializeOWLs"; import { OWL } from "./types/owl"; import { UseOWLs, useOWLs } from "./utils/owl"; @@ -106,6 +107,8 @@ export default function OWLCommand({ >) { const { callbackLaunchOptions } = launchContext; + useInitializeOWLs(); + const { value: currentLanguage, isLoading } = useCurrentLanguage(); const { owls, pushHistory } = useOWLs(); diff --git a/extensions/owl/src/types/storage.ts b/extensions/owl/src/types/storage.ts index 56294fab18b..9ba3b5e31cd 100644 --- a/extensions/owl/src/types/storage.ts +++ b/extensions/owl/src/types/storage.ts @@ -5,6 +5,7 @@ export type UnknownRecord = Record; export enum StorageKey { OWLS = "owls", + INIT = "init", } export type StructuredValue = UnknownRecord | UnknownRecord[]; diff --git a/extensions/owl/src/utils/loadDefaultOWLs.ts b/extensions/owl/src/utils/loadDefaultOWLs.ts new file mode 100644 index 00000000000..baa8d496fc0 --- /dev/null +++ b/extensions/owl/src/utils/loadDefaultOWLs.ts @@ -0,0 +1,57 @@ +import { Alert, confirmAlert } from "@raycast/api"; +import { randomUUID } from "node:crypto"; +import { OWL, OWLMapping } from "../types/owl"; +import { UseOWLs } from "./owl"; + +export async function loadDefaultOWLs({ + languages, + keyboards, + setOWLs, + showAlert = true, +}: { + languages: string[]; + keyboards: string[]; + setOWLs: UseOWLs["setOWLs"]; + showAlert?: boolean; +}): Promise { + const defaultMapping: OWLMapping = Object.fromEntries( + languages.map((language) => { + return [ + language, + languages + .flatMap((destinationLanguage) => { + return keyboards.filter((keyboard) => keyboard === destinationLanguage && keyboard !== language); + }) + .map( + (keyboard): OWL => ({ + id: randomUUID(), + from: language, + to: keyboard, + history: [], + }), + ), + ]; + }), + ); + + if (showAlert) { + const shouldLoadDefault = await confirmAlert({ + title: "Are you sure?", + message: `Are you sure you want to reset the OWLs to the default mapping?\nThis will remove all existing OWLs + and add all permutations of your existing languages.`, + primaryAction: { + title: "Reset", + style: Alert.ActionStyle.Destructive, + }, + }); + + // The user has been prompted and choose not to load defaults. + if (!shouldLoadDefault) { + return false; + } + } + + setOWLs(defaultMapping); + + return true; +}