mirror of
https://github.com/baairon/torlink.git
synced 2026-07-08 18:28:22 +02:00
feat: terminal-native torrent discovery TUI
Curated, multi-source torrent search and download from the terminal: concurrent search across built-in sources with streamed, source-tagged results; a download-only webtorrent engine with true pause/resume and restart recovery; magnet paste; and a calm Ink + React TUI. No indexer server, no setup.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { useState } from "react";
|
||||
import { Text, useInput } from "ink";
|
||||
|
||||
export interface TextFieldProps {
|
||||
isDisabled?: boolean;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: (value: string) => void;
|
||||
}
|
||||
|
||||
interface Edit {
|
||||
value: string;
|
||||
cursor: number;
|
||||
}
|
||||
|
||||
export function deleteBefore(value: string, cursor: number): Edit {
|
||||
if (cursor === 0) return { value, cursor };
|
||||
return {
|
||||
value: value.slice(0, cursor - 1) + value.slice(cursor),
|
||||
cursor: cursor - 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteWordBefore(value: string, cursor: number): Edit {
|
||||
let i = cursor;
|
||||
while (i > 0 && value[i - 1] === " ") i--;
|
||||
while (i > 0 && value[i - 1] !== " ") i--;
|
||||
return { value: value.slice(0, i) + value.slice(cursor), cursor: i };
|
||||
}
|
||||
|
||||
export function killToEnd(value: string, cursor: number): Edit {
|
||||
return { value: value.slice(0, cursor), cursor };
|
||||
}
|
||||
|
||||
export function insertAt(value: string, cursor: number, text: string): Edit {
|
||||
return {
|
||||
value: value.slice(0, cursor) + text + value.slice(cursor),
|
||||
cursor: cursor + text.length,
|
||||
};
|
||||
}
|
||||
|
||||
const CURSOR = " ";
|
||||
|
||||
export function TextField({
|
||||
isDisabled = false,
|
||||
defaultValue = "",
|
||||
placeholder = "",
|
||||
onChange,
|
||||
onSubmit,
|
||||
}: TextFieldProps) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [cursor, setCursor] = useState(defaultValue.length);
|
||||
|
||||
function apply(next: Edit): void {
|
||||
setValue(next.value);
|
||||
setCursor(Math.max(0, Math.min(next.value.length, next.cursor)));
|
||||
if (next.value !== value) onChange?.(next.value);
|
||||
}
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (key.upArrow || key.downArrow || key.tab || (key.ctrl && input === "c"))
|
||||
return;
|
||||
|
||||
if (key.return) {
|
||||
onSubmit?.(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl) {
|
||||
switch (input) {
|
||||
case "u":
|
||||
apply({ value: "", cursor: 0 });
|
||||
return;
|
||||
case "w":
|
||||
apply(deleteWordBefore(value, cursor));
|
||||
return;
|
||||
case "k":
|
||||
apply(killToEnd(value, cursor));
|
||||
return;
|
||||
case "a":
|
||||
setCursor(0);
|
||||
return;
|
||||
case "e":
|
||||
setCursor(value.length);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.leftArrow) {
|
||||
setCursor(Math.max(0, cursor - 1));
|
||||
return;
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursor(Math.min(value.length, cursor + 1));
|
||||
return;
|
||||
}
|
||||
if (key.backspace || key.delete) {
|
||||
apply(deleteBefore(value, cursor));
|
||||
return;
|
||||
}
|
||||
if (key.meta || !input) return;
|
||||
const text = input.replace(/\x1b?\[<\d+;\d+;\d+[Mm]/g, "");
|
||||
if (!text) return;
|
||||
apply(insertAt(value, cursor, text));
|
||||
},
|
||||
{ isActive: !isDisabled },
|
||||
);
|
||||
|
||||
if (isDisabled) {
|
||||
return value ? <Text>{value}</Text> : <Text dimColor>{placeholder}</Text>;
|
||||
}
|
||||
|
||||
if (value.length === 0) {
|
||||
if (placeholder) {
|
||||
return (
|
||||
<Text>
|
||||
<Text inverse>{placeholder[0]}</Text>
|
||||
<Text dimColor>{placeholder.slice(1)}</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return <Text inverse>{CURSOR}</Text>;
|
||||
}
|
||||
|
||||
const before = value.slice(0, cursor);
|
||||
const atChar = value[cursor] ?? CURSOR;
|
||||
const after = cursor < value.length ? value.slice(cursor + 1) : "";
|
||||
return (
|
||||
<Text>
|
||||
{before}
|
||||
<Text inverse>{atChar}</Text>
|
||||
{after}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user