mirror of
https://github.com/baairon/torlink.git
synced 2026-07-08 18:28:22 +02:00
feat: added 'Copy magnet' option via 'y' key
This commit is contained in:
@@ -97,6 +97,7 @@ function makeStore(
|
||||
seedFocus: null,
|
||||
setSeedFocus: noop,
|
||||
startDownload: noop,
|
||||
copyMagnet: noop,
|
||||
notice: null,
|
||||
setNotice: noop,
|
||||
quitAll: noop,
|
||||
|
||||
+14
-1
@@ -8,7 +8,7 @@ import { loadHistory } from "../download/history";
|
||||
import { reconcileQueue } from "../download/reconcile";
|
||||
import { parseMagnet } from "../sources/magnet";
|
||||
import { magnetFromTorrentFile } from "../sources/torrentFile";
|
||||
import { readClipboard } from "../util/clipboard";
|
||||
import { readClipboard, writeClipboard } from "../util/clipboard";
|
||||
import { cleanText, truncate } from "../util/format";
|
||||
import {
|
||||
StoreContext,
|
||||
@@ -168,6 +168,17 @@ export function App({
|
||||
[config, queue],
|
||||
);
|
||||
|
||||
const copyMagnet = useCallback((input: { name: string; magnet: string }) => {
|
||||
void (async () => {
|
||||
const ok = await writeClipboard(input.magnet);
|
||||
if (ok) {
|
||||
setNotice(`Copied magnet: ${truncate(cleanText(input.magnet), 60)}`);
|
||||
return;
|
||||
}
|
||||
setNotice(`Couldn't copy magnet for ${truncate(cleanText(input.name), 32)}.`);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const submitQuery = useCallback(
|
||||
(raw: string) => {
|
||||
const q = raw.trim();
|
||||
@@ -247,6 +258,7 @@ export function App({
|
||||
seedFocus,
|
||||
setSeedFocus,
|
||||
startDownload,
|
||||
copyMagnet,
|
||||
notice,
|
||||
setNotice,
|
||||
quitAll,
|
||||
@@ -269,6 +281,7 @@ export function App({
|
||||
downloadFocus,
|
||||
seedFocus,
|
||||
startDownload,
|
||||
copyMagnet,
|
||||
notice,
|
||||
listRows,
|
||||
compact,
|
||||
|
||||
@@ -80,6 +80,14 @@ function Detail({ r, width }: { r: TorrentResult; width: number }) {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Magnet"
|
||||
value={
|
||||
<Text color={COLOR.alt} dimColor wrap="truncate-end">
|
||||
{r.magnet}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={COLOR.accent} bold>
|
||||
@@ -87,6 +95,11 @@ function Detail({ r, width }: { r: TorrentResult; width: number }) {
|
||||
</Text>
|
||||
<Text color={COLOR.text}> Download</Text>
|
||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
||||
<Text color={COLOR.accent} bold>
|
||||
y
|
||||
</Text>
|
||||
<Text color={COLOR.text}> Copy magnet</Text>
|
||||
<Text dimColor>{` ${ICON.dot} `}</Text>
|
||||
<Text color={COLOR.alt}>esc</Text>
|
||||
<Text dimColor> back</Text>
|
||||
</Box>
|
||||
@@ -102,6 +115,7 @@ export function Results() {
|
||||
region,
|
||||
setCaptureMode,
|
||||
startDownload,
|
||||
copyMagnet,
|
||||
contentWidth,
|
||||
listRows,
|
||||
} = useStore();
|
||||
@@ -149,6 +163,9 @@ export function Results() {
|
||||
sizeBytes: r.sizeBytes,
|
||||
});
|
||||
|
||||
const copyResultMagnet = (r: TorrentResult): void =>
|
||||
copyMagnet({ name: r.name, magnet: r.magnet });
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (input === "/") {
|
||||
@@ -169,6 +186,9 @@ export function Results() {
|
||||
} else if (input === "d") {
|
||||
const r = results[clamped];
|
||||
if (r) openDownload(r);
|
||||
} else if (input === "y") {
|
||||
const r = results[clamped];
|
||||
if (r) copyResultMagnet(r);
|
||||
}
|
||||
},
|
||||
{ isActive: focused && mode === "list" },
|
||||
@@ -180,6 +200,7 @@ export function Results() {
|
||||
setMode("list");
|
||||
setDetail(null);
|
||||
} else if (input === "d" && detail) openDownload(detail);
|
||||
else if (input === "y" && detail) copyResultMagnet(detail);
|
||||
},
|
||||
{ isActive: focused && mode === "detail" },
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
||||
hints: [
|
||||
{ keys: "/", label: "Edit search" },
|
||||
{ keys: "↵", label: "Run search" },
|
||||
{ keys: "y", label: "Copy magnet" },
|
||||
{ keys: "m", label: "Paste magnet" },
|
||||
],
|
||||
},
|
||||
@@ -92,6 +93,7 @@ export function footerHints(
|
||||
}
|
||||
return [
|
||||
{ keys: "d", label: "Download" },
|
||||
{ keys: "y", label: "Copy magnet" },
|
||||
{ keys: "/", label: "Search" },
|
||||
{ keys: "m", label: "Paste magnet" },
|
||||
SWITCH,
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface Store {
|
||||
source?: SourceId;
|
||||
sizeBytes?: number;
|
||||
}) => void;
|
||||
copyMagnet: (input: { name: string; magnet: string }) => void;
|
||||
|
||||
notice: string | null;
|
||||
setNotice: (s: string | null) => void;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const spawn = vi.fn();
|
||||
|
||||
vi.mock("node:child_process", () => ({ spawn }));
|
||||
|
||||
describe("writeClipboard", () => {
|
||||
it("writes text to the first available Linux clipboard command", async () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
try {
|
||||
spawn.mockImplementation((cmd: string) => {
|
||||
const proc = new EventEmitter() as EventEmitter & {
|
||||
stdin: { end: (value: string) => void };
|
||||
stdout: EventEmitter;
|
||||
kill: () => void;
|
||||
};
|
||||
proc.stdout = new EventEmitter();
|
||||
proc.kill = vi.fn();
|
||||
proc.stdin = {
|
||||
end: vi.fn(() => {
|
||||
queueMicrotask(() => proc.emit("close", cmd === "wl-copy" ? 0 : 1));
|
||||
}),
|
||||
};
|
||||
return proc;
|
||||
});
|
||||
|
||||
const { writeClipboard } = await import("./clipboard");
|
||||
|
||||
await expect(writeClipboard("magnet:?xt=urn:btih:abc")).resolves.toBe(true);
|
||||
expect(spawn).toHaveBeenCalledWith("wl-copy", [], { windowsHide: true });
|
||||
expect(spawn.mock.results[0]?.value.stdin.end).toHaveBeenCalledWith(
|
||||
"magnet:?xt=urn:btih:abc",
|
||||
);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
vi.resetModules();
|
||||
spawn.mockReset();
|
||||
}
|
||||
});
|
||||
});
|
||||
+53
-2
@@ -27,12 +27,46 @@ function run(cmd: string, args: string[]): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
const LINUX: [string, string[]][] = [
|
||||
function write(cmd: string, args: string[], text: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const proc = spawn(cmd, args, { windowsHide: true });
|
||||
let settled = false;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
const done = (ok: boolean): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve(ok);
|
||||
};
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
proc.kill();
|
||||
} catch {}
|
||||
done(false);
|
||||
}, 4000);
|
||||
timer.unref?.();
|
||||
proc.on("error", () => done(false));
|
||||
proc.on("close", (code) => done(code === 0));
|
||||
proc.stdin?.end(text);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const LINUX_READ: [string, string[]][] = [
|
||||
["wl-paste", ["--no-newline"]],
|
||||
["xclip", ["-selection", "clipboard", "-o"]],
|
||||
["xsel", ["-b"]],
|
||||
];
|
||||
|
||||
const LINUX_WRITE: [string, string[]][] = [
|
||||
["wl-copy", []],
|
||||
["xclip", ["-selection", "clipboard"]],
|
||||
["xsel", ["-b", "-i"]],
|
||||
];
|
||||
|
||||
export async function readClipboard(): Promise<string> {
|
||||
if (process.platform === "win32") {
|
||||
return (await run("powershell", ["-NoProfile", "-Command", "Get-Clipboard"])).trim();
|
||||
@@ -40,9 +74,26 @@ export async function readClipboard(): Promise<string> {
|
||||
if (process.platform === "darwin") {
|
||||
return (await run("pbpaste", [])).trim();
|
||||
}
|
||||
for (const [cmd, args] of LINUX) {
|
||||
for (const [cmd, args] of LINUX_READ) {
|
||||
const out = (await run(cmd, args)).trim();
|
||||
if (out) return out;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function writeClipboard(text: string): Promise<boolean> {
|
||||
if (process.platform === "win32") {
|
||||
return write(
|
||||
"powershell",
|
||||
["-NoProfile", "-Command", "Set-Clipboard -Value ([Console]::In.ReadToEnd())"],
|
||||
text,
|
||||
);
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
return write("pbcopy", [], text);
|
||||
}
|
||||
for (const [cmd, args] of LINUX_WRITE) {
|
||||
if (await write(cmd, args, text)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user