Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/owl/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 4 additions & 1 deletion extensions/owl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
46 changes: 36 additions & 10 deletions extensions/owl/src/components/AddOWL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,17 +20,23 @@ export function AddOWL(props: Readonly<{ base?: string }>) {
const { keyboards } = useKeyboards();
const [, setOWLs] = useCachedStorage<OWLMapping>(StorageKey.OWLS, {});

const { value: languages } = useLanguages();
const [showAll, setShowAll] = useState<boolean>(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;
}

return a.localeCompare(b);
});
}, [base, keyboards]);
}, [base, keyboards, showAll, languages]);

const [from, setFrom] = useState<string>(base ? (fromOptions?.[0] ?? "") : "");
const [to, setTo] = useState<string>("");
Expand Down Expand Up @@ -85,35 +92,54 @@ export function AddOWL(props: Readonly<{ base?: string }>) {
push(<ViewKeyboard keyboard={to} />);
}}
/>
<Action
title={showAll ? "Hide Extra Keyboards" : "Show All Keyboards"}
shortcut={{
modifiers: ["cmd", "shift"],
key: "a",
}}
onAction={() => {
setShowAll(!showAll);
}}
/>
</ActionPanel>
}
>
<Form.Dropdown id={"from"} title={"From"} value={from} onChange={setFrom}>
<Form.Dropdown.Section title={"Suggested"}>
<Form.Dropdown.Section title={showAll ? "Suggested" : undefined}>
{fromOptions.toSorted(lengthLocaleCompare).map((keyboard) => {
return <Form.Dropdown.Item key={keyboard} value={keyboard} title={keyboard} />;
})}
</Form.Dropdown.Section>
<Form.Dropdown.Section title={"All"}>
{keyboards
.filter((keyboard) => !fromOptions.includes(keyboard))
.toSorted(lengthLocaleCompare)
.map((keyboard) => {
return <Form.Dropdown.Item key={keyboard} value={keyboard} title={keyboard} />;
})}
</Form.Dropdown.Section>
{showAll && (
<Form.Dropdown.Section title={"All"}>
{keyboards
.filter((keyboard) => !fromOptions.includes(keyboard))
.toSorted(lengthLocaleCompare)
.map((keyboard) => {
return <Form.Dropdown.Item key={keyboard} value={keyboard} title={keyboard} />;
})}
</Form.Dropdown.Section>
)}
</Form.Dropdown>

<Form.Dropdown id={"to"} title={"To"} value={to} onChange={setTo}>
{keyboards
.filter((keyboard) => {
return keyboard !== from;
})
.filter((keyboard) => {
return showAll || languages.includes(keyboard);
})
.toSorted(lengthLocaleCompare)
.map((keyboard) => {
return <Form.Dropdown.Item key={keyboard} value={keyboard} title={keyboard} />;
})}
</Form.Dropdown>

<Form.Separator />

<Form.Checkbox id="showAll" label="Show all keyboards?" value={showAll} onChange={setShowAll} />
</Form>
);
}
34 changes: 34 additions & 0 deletions extensions/owl/src/components/ClearAllOWLsAction.tsx
Original file line number Diff line number Diff line change
@@ -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<OWLMapping>(StorageKey.OWLS, {});

return (
<Action
title={"Clear Owls"}
style={Action.Style.Destructive}
icon={Icon.Trash}
shortcut={{
modifiers: ["ctrl", "shift"],
key: "x",
}}
onAction={async () => {
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({});
}
}}
/>
);
}
36 changes: 36 additions & 0 deletions extensions/owl/src/components/DeleteAllOWLsAction.tsx
Original file line number Diff line number Diff line change
@@ -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<OWLMapping>(StorageKey.OWLS, {});

return (
<Action
title={"Delete Owls"}
style={Action.Style.Destructive}
icon={Icon.Trash}
shortcut={Keyboard.Shortcut.Common.Remove}
onAction={async () => {
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;
});
Comment thread
gohadar marked this conversation as resolved.
}
}}
/>
);
}
39 changes: 39 additions & 0 deletions extensions/owl/src/components/ResetToDefaultOWLAction.tsx
Original file line number Diff line number Diff line change
@@ -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<Action.Props, "title" | "onAction"> & {
base?: string;
}
>,
) {
const { keyboards } = useKeyboards();
const { value: languages } = useLanguages();
const [, setOWLs] = useCachedStorage<OWLMapping>(StorageKey.OWLS, {});

return (
<Action
title={"Reset to Default"}
style={Action.Style.Destructive}
icon={Icon.RotateAntiClockwise}
shortcut={{
modifiers: ["cmd", "shift"],
key: "r",
}}
{...props}
onAction={async () => {
await loadDefaultOWLs({
keyboards,
languages,
setOWLs,
});
}}
/>
);
}
12 changes: 12 additions & 0 deletions extensions/owl/src/configure-owls.tsx
Original file line number Diff line number Diff line change
@@ -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<OWLMapping>(StorageKey.OWLS, {});
const { value: languages, isLoading } = useLanguages();

Expand All @@ -17,6 +23,7 @@ export default function ConfigureOWLsCommand() {
actions={
<ActionPanel>
<AddOWLAction />
<ResetToDefaultOWLAction />
</ActionPanel>
}
/>
Expand All @@ -31,6 +38,11 @@ export default function ConfigureOWLsCommand() {
<Action.Push title={"View Owls"} icon={Icon.List} target={<ViewOWLs language={language} />} />
)}
<AddOWLAction base={language} />
<ActionPanel.Section>
{owls[language] !== undefined && <DeleteAllOWLsAction language={language} />}
<ResetToDefaultOWLAction />
<ClearAllOWLsAction />
</ActionPanel.Section>
</ActionPanel>
}
accessories={(owls[language] ?? [])
Expand Down
44 changes: 44 additions & 0 deletions extensions/owl/src/hooks/useInitializeOWLs.ts
Original file line number Diff line number Diff line change
@@ -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<OWLMapping>(StorageKey.OWLS, {});
const [isInit, setIsInit] = useCachedStorage<boolean>(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);
},
};
}
3 changes: 3 additions & 0 deletions extensions/owl/src/owl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -106,6 +107,8 @@ export default function OWLCommand({
>) {
const { callbackLaunchOptions } = launchContext;

useInitializeOWLs();

const { value: currentLanguage, isLoading } = useCurrentLanguage();
const { owls, pushHistory } = useOWLs();

Expand Down
1 change: 1 addition & 0 deletions extensions/owl/src/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type UnknownRecord = Record<PropertyKey, unknown>;

export enum StorageKey {
OWLS = "owls",
INIT = "init",
}

export type StructuredValue = UnknownRecord | UnknownRecord[];
Expand Down
57 changes: 57 additions & 0 deletions extensions/owl/src/utils/loadDefaultOWLs.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}