Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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] - {PR_MERGE_DATE}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unfilled template placeholder

{PR_MERGE_DATE} is a template token that should be replaced with the actual merge date before the PR is merged.

Suggested change
## [Filter Keyboards] - {PR_MERGE_DATE}
## [Filter Keyboards] - 2026-04-23
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/owl/CHANGELOG.md
Line: 3

Comment:
**Unfilled template placeholder**

`{PR_MERGE_DATE}` is a template token that should be replaced with the actual merge date before the PR is merged.

```suggestion
## [Filter Keyboards] - 2026-04-23
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pernielsentikaer, should I update this manually or is this part of the CI/CD? I remember it updating automatically when the merge is done but I'm not quite sure.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@greptileai that's NOT correct, why do you say that? You should know that {PR_MERGE_DATE} is the right thing here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — {PR_MERGE_DATE} is the standard Raycast convention and gets automatically populated at merge time. My original comment was incorrect; no changes needed here.

Tip: You can give me more context with .greptile/rules so I get this right next time.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@greptileai, so you already know this from your knowledge base, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — rule 799af734 in my knowledge base explicitly states that {PR_MERGE_DATE} should be left as-is, as it's the standard Raycast convention populated automatically at merge time. My original comment was wrong despite having that context.


- 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
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;
}
Loading