diff --git a/apps/yaak-client/commands/importCurl.ts b/apps/yaak-client/commands/importCurl.ts
new file mode 100644
index 00000000..c264c43f
--- /dev/null
+++ b/apps/yaak-client/commands/importCurl.ts
@@ -0,0 +1,52 @@
+import type { HttpRequest } from "@yaakapp-internal/models";
+import { patchModelById } from "@yaakapp-internal/models";
+import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
+import { createFastMutation } from "../hooks/useFastMutation";
+import { wasUpdatedExternally } from "../hooks/useRequestUpdateKey";
+import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
+import { jotaiStore } from "../lib/jotai";
+import { invokeCmd } from "../lib/tauri";
+import { showToast } from "../lib/toast";
+
+export function looksLikeCurl(text: string) {
+ return text.trim().startsWith("curl ");
+}
+
+export const importCurl = createFastMutation<
+ void,
+ string,
+ { overwriteRequestId?: string; command: string }
+>({
+ mutationKey: ["import_curl"],
+ mutationFn: async ({ overwriteRequestId, command }) => {
+ const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
+ const importedRequest: HttpRequest = await invokeCmd("cmd_curl_to_request", {
+ command,
+ workspaceId,
+ });
+
+ let verb: string;
+ if (overwriteRequestId == null) {
+ verb = "Created";
+ await createRequestAndNavigate(importedRequest);
+ } else {
+ verb = "Updated";
+ await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
+ ...importedRequest,
+ id: r.id,
+ createdAt: r.createdAt,
+ workspaceId: r.workspaceId,
+ folderId: r.folderId,
+ name: r.name,
+ sortPriority: r.sortPriority,
+ }));
+
+ setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
+ }
+
+ showToast({
+ color: "success",
+ message: `${verb} request from Curl`,
+ });
+ },
+});
diff --git a/apps/yaak-client/components/HttpRequestPane.tsx b/apps/yaak-client/components/HttpRequestPane.tsx
index 9c73697b..c6d25c3f 100644
--- a/apps/yaak-client/components/HttpRequestPane.tsx
+++ b/apps/yaak-client/components/HttpRequestPane.tsx
@@ -5,11 +5,11 @@ import classNames from "classnames";
import { atom, useAtomValue } from "jotai";
import type { CSSProperties } from "react";
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
+import { importCurl, looksLikeCurl } from "../commands/importCurl";
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
import { useAuthTab } from "../hooks/useAuthTab";
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useHeadersTab } from "../hooks/useHeadersTab";
-import { useImportCurl } from "../hooks/useImportCurl";
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
@@ -278,7 +278,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
const updateKey = useRequestUpdateKey(activeRequestId);
- const { mutate: importCurl } = useImportCurl();
const handleBodyChange = useCallback(
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
@@ -299,8 +298,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const handlePaste = useCallback(
async (e: ClipboardEvent, text: string) => {
- if (text.startsWith("curl ")) {
- importCurl({ overwriteRequestId: activeRequestId, command: text });
+ if (looksLikeCurl(text)) {
+ importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
} else {
const patch = prepareImportQuerystring(text);
if (patch != null) {
@@ -322,7 +321,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
}
}
},
- [activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh, importCurl],
+ [activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh],
);
const handleSend = useCallback(
() => sendRequest(activeRequest.id ?? null),
diff --git a/apps/yaak-client/components/ImportCurl.tsx b/apps/yaak-client/components/ImportCurl.tsx
new file mode 100644
index 00000000..5ddd1990
--- /dev/null
+++ b/apps/yaak-client/components/ImportCurl.tsx
@@ -0,0 +1,113 @@
+import { isTauri } from "@tauri-apps/api/core";
+import { readText } from "@tauri-apps/plugin-clipboard-manager";
+import { Icon } from "@yaakapp-internal/ui";
+import * as m from "motion/react-m";
+import { useEffect, useState } from "react";
+import { importCurl, looksLikeCurl } from "../commands/importCurl";
+import { useWindowFocus } from "../hooks/useWindowFocus";
+import { showToast } from "../lib/toast";
+import { Button } from "./core/Button";
+
+/**
+ * Offers to create a request from a Curl command on the clipboard. The desktop app can
+ * read the clipboard whenever the window is focused, but a browser can't without
+ * prompting for permission, so it waits for the user to paste instead.
+ */
+export function ImportCurl() {
+ return isTauri() ? : ;
+}
+
+function ImportCurlButton() {
+ const focused = useWindowFocus();
+ const [clipboardText, setClipboardText] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- none
+ useEffect(() => {
+ void readText().then(setClipboardText);
+ }, [focused]);
+
+ if (!looksLikeCurl(clipboardText)) {
+ return null;
+ }
+
+ return (
+
+ }
+ isLoading={isLoading}
+ title="Import Curl command from clipboard"
+ onClick={async () => {
+ setIsLoading(true);
+ try {
+ await importCurl.mutateAsync({ command: clipboardText });
+ setClipboardText(""); // Hide the button until the clipboard changes
+ } catch (e) {
+ console.log("Failed to import curl", e);
+ } finally {
+ setIsLoading(false);
+ }
+ }}
+ >
+ Import Curl
+
+
+ );
+}
+
+function ImportCurlOnPaste() {
+ useEffect(() => {
+ const handlePaste = (e: ClipboardEvent) => {
+ const command = e.clipboardData?.getData("text/plain") ?? "";
+ if (!looksLikeCurl(command)) return;
+
+ // Editable targets keep the text as text. The URL editor imports it itself.
+ if (isEditable(e.target)) return;
+
+ showToast({
+ id: "curl-paste",
+ color: "success",
+ message: (
+
+
Curl command detected
+
Create a request from the pasted command?
+
+ ),
+ action: ({ hide }) => (
+ }
+ onClick={() => {
+ hide();
+ importCurl.mutate({ command });
+ }}
+ >
+ Create Request
+
+ ),
+ });
+ };
+
+ document.addEventListener("paste", handlePaste);
+ return () => document.removeEventListener("paste", handlePaste);
+ }, []);
+
+ return null;
+}
+
+function isEditable(target: EventTarget | null) {
+ if (!(target instanceof HTMLElement)) return false;
+ if (target.isContentEditable) return true;
+ if (target.closest(".cm-editor") != null) return true;
+ return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
+}
diff --git a/apps/yaak-client/components/ImportCurlButton.tsx b/apps/yaak-client/components/ImportCurlButton.tsx
deleted file mode 100644
index 28575c70..00000000
--- a/apps/yaak-client/components/ImportCurlButton.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { clear, readText } from "@tauri-apps/plugin-clipboard-manager";
-import * as m from "motion/react-m";
-import { useEffect, useState } from "react";
-import { useImportCurl } from "../hooks/useImportCurl";
-import { useWindowFocus } from "../hooks/useWindowFocus";
-import { Button } from "./core/Button";
-import { Icon } from "@yaakapp-internal/ui";
-
-export function ImportCurlButton() {
- const focused = useWindowFocus();
- const [clipboardText, setClipboardText] = useState("");
-
- const importCurl = useImportCurl();
- const [isLoading, setIsLoading] = useState(false);
-
- // oxlint-disable-next-line react-hooks/exhaustive-deps -- none
- useEffect(() => {
- void readText().then(setClipboardText);
- }, [focused]);
-
- if (!clipboardText?.trim().startsWith("curl ")) {
- return null;
- }
-
- return (
-
- }
- isLoading={isLoading}
- title="Import Curl command from clipboard"
- onClick={async () => {
- setIsLoading(true);
- try {
- await importCurl.mutateAsync({ command: clipboardText });
- await clear(); // Clear the clipboard so the button goes away
- setClipboardText("");
- } catch (e) {
- console.log("Failed to import curl", e);
- } finally {
- setIsLoading(false);
- }
- }}
- >
- Import Curl
-
-
- );
-}
diff --git a/apps/yaak-client/components/WorkspaceHeader.tsx b/apps/yaak-client/components/WorkspaceHeader.tsx
index 5c79057e..aefe5f4e 100644
--- a/apps/yaak-client/components/WorkspaceHeader.tsx
+++ b/apps/yaak-client/components/WorkspaceHeader.tsx
@@ -10,7 +10,7 @@ import { CookieDropdown } from "./CookieDropdown";
import { IconButton } from "./core/IconButton";
import { PillButton } from "./core/PillButton";
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
-import { ImportCurlButton } from "./ImportCurlButton";
+import { ImportCurl } from "./ImportCurl";
import { LicenseBadge } from "./LicenseBadge";
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
import { SettingsDropdown } from "./SettingsDropdown";
@@ -56,7 +56,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({
-
+
{showEncryptionSetup ? (
Enter Encryption Key
diff --git a/apps/yaak-client/hooks/useImportCurl.ts b/apps/yaak-client/hooks/useImportCurl.ts
deleted file mode 100644
index 545aa9af..00000000
--- a/apps/yaak-client/hooks/useImportCurl.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { HttpRequest } from "@yaakapp-internal/models";
-import { patchModelById } from "@yaakapp-internal/models";
-import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
-import { jotaiStore } from "../lib/jotai";
-import { invokeCmd } from "../lib/tauri";
-import { showToast } from "../lib/toast";
-import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
-import { useFastMutation } from "./useFastMutation";
-import { wasUpdatedExternally } from "./useRequestUpdateKey";
-
-export function useImportCurl() {
- return useFastMutation({
- mutationKey: ["import_curl"],
- mutationFn: async ({
- overwriteRequestId,
- command,
- }: {
- overwriteRequestId?: string;
- command: string;
- }) => {
- const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
- const importedRequest: HttpRequest = await invokeCmd("cmd_curl_to_request", {
- command,
- workspaceId,
- });
-
- let verb: string;
- if (overwriteRequestId == null) {
- verb = "Created";
- await createRequestAndNavigate(importedRequest);
- } else {
- verb = "Updated";
- await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
- ...importedRequest,
- id: r.id,
- createdAt: r.createdAt,
- workspaceId: r.workspaceId,
- folderId: r.folderId,
- name: r.name,
- sortPriority: r.sortPriority,
- }));
-
- setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
- }
-
- showToast({
- color: "success",
- message: `${verb} request from Curl`,
- });
- },
- });
-}