feat: wordmark + progress-bar sheen, dark-violet svg base, readme refinements

This commit is contained in:
bairon.dev
2026-06-27 12:42:38 -04:00
parent ea12f2f134
commit 8a9886be9c
11 changed files with 211 additions and 43 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 bairon.dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+6 -11
View File
@@ -2,7 +2,7 @@
<img src="preview/splash.svg" alt="torlink, curated torrents straight from your terminal" style="max-width: 832px; width: 100%; height: auto;">
</p>
torlink is a torrent finder that lives in your terminal, with zero setup and nothing to configure. One search checks a short, curated list of reputable sources at once, and whatever you pick downloads straight to your computer. The files are yours, saved to your downloads folder.
torlink is a torrent finder that lives in your terminal, with zero setup and nothing to configure. One search checks a short, curated list of reputable sources at once, and whatever you pick downloads straight to your computer, never seeding back out. The files are yours, saved to your downloads folder.
## Get started
@@ -22,16 +22,9 @@ npx torlink "<path/to/file.torrent>" # open a .torrent file
npm install -g torlink # install for good, then run: torlink
```
## What it does
- **One search, every source.** Type once and torlink checks them all together, showing each result's size and how many people are sharing it.
- **Curated, not a free-for-all.** A small, vetted set of sources, tagged so you always know where a result came from.
- **Downloads only, never seeds.** Files come to your computer and stop there. torlink never sends them back out.
- **Nothing to configure.** It opens straight to a search box, ready the moment it starts.
## Finding something
Type what you're looking for and press Enter. torlink checks every source at once, and results show up as each one answers, so you're never stuck waiting on a slow one. Each result lists its size and how many people are sharing it, so you can see which one will come down fast. Arrow to the one you want, press `d`, and it saves to your downloads folder. Leave the box empty and press Enter to just see what's new across every source.
Type what you're looking for and press Enter. Results stream in from every source as they answer, tagged with size and how many people are sharing each one, so you can see what'll come down fast. Arrow to what you want and press `d` to save it.
<p align="center">
<img src="preview/browse.svg" alt="torlink's browse view: the sidebar, the search bar, and merged results from every source" style="max-width: 832px; width: 100%; height: auto;">
@@ -39,7 +32,9 @@ Type what you're looking for and press Enter. torlink checks every source at onc
## Your downloads
Your active downloads and the stuff you've already grabbed share one pane. Whatever's still coming down sits up top with its progress, speed, and time left. When a file finishes it drops into Recently downloaded just below, so the list stays tidy instead of piling up. Anything there is one key from downloading again, and it's all still waiting when you come back, resuming anything that got interrupted.
Active downloads sit up top with their progress, speed, and time left; when one finishes it drops into Recently downloaded just below, so the list stays tidy. Everything's still there when you come back, and anything interrupted picks up where it left off.
Downloads run in the background while you keep searching, so you can queue up as many as you want. They save to your downloads folder, and the Downloads pane keeps tabs on each one.
<p align="center">
<img src="preview/downloads.svg" alt="torlink's Downloads pane: live progress on top, recently downloaded below" style="max-width: 832px; width: 100%; height: auto;">
@@ -56,7 +51,7 @@ A short, hand-picked list of trusted sources:
| TV | EZTV, SolidTorrents, The Pirate Bay, 1337x |
| Anime | Nyaa, SubsPlease |
Games are the only category that can run code, so they come from FitGirl alone, a repacker with a long, trusted track record; everything else is plain video and subtitles. A source being down never stops a search, the others keep going and torlink tells you which one is offline.
Games are the only category that can run code, so they come from FitGirl alone, a repacker with a long, trusted track record; everything else is plain video and subtitles. If a source is down, the search carries on without it, and torlink tells you which one is offline.
## Local Development
+3 -3
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="832" height="540" viewBox="0 0 832 540" style="max-width: 832px; width: 100%; height: auto;" role="img">
<rect width="100%" height="100%" rx="14" fill="#0c0b14"/>
<rect x="0.5" y="0.5" width="831" height="539" rx="14" fill="none" stroke="#272430" stroke-width="1"/>
<rect width="100%" height="100%" rx="14" fill="#0a0810"/>
<rect x="0.5" y="0.5" width="831" height="539" rx="14" fill="none" stroke="#2d2b31" stroke-width="1"/>
<text x="416" y="23" text-anchor="middle" font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="13" fill="#8a8a8a">torlink</text>
<g font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="16" fill="#ddd8ea">
<text x="99.2" y="84" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#5ae87a" font-weight="600">𐓏</text>
@@ -47,7 +47,7 @@
<text x="60.8" y="216" textLength="48" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Games</text>
<rect x="208.9" y="200" width="1.4" height="22" fill="#6b6577"/>
<text x="224" y="216" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#a78bfa"></text>
<text x="243.2" y="216" textLength="249.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Search, or paste a magnet…</text>
<text x="243.2" y="216" textLength="288" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Search or paste a magnet link</text>
<rect x="784.9" y="200" width="1.4" height="22" fill="#6b6577"/>
<text x="60.8" y="238" textLength="57.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Movies</text>
<text x="204.8" y="238" textLength="585.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#6b6577">╰───────────────────────────────────────────────────────────╯</text>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+44 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="832" height="452" viewBox="0 0 832 452" style="max-width: 832px; width: 100%; height: auto;" role="img">
<rect width="100%" height="100%" rx="14" fill="#0c0b14"/>
<rect x="0.5" y="0.5" width="831" height="451" rx="14" fill="none" stroke="#272430" stroke-width="1"/>
<rect width="100%" height="100%" rx="14" fill="#0a0810"/>
<rect x="0.5" y="0.5" width="831" height="451" rx="14" fill="none" stroke="#2d2b31" stroke-width="1"/>
<text x="416" y="23" text-anchor="middle" font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="13" fill="#8a8a8a">torlink</text>
<g font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="16" fill="#ddd8ea">
<text x="99.2" y="84" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#5ae87a" font-weight="600">𐓏</text>
@@ -108,4 +108,46 @@
<text x="416" y="414" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6">?</text>
<text x="435.2" y="414" textLength="38.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">Keys</text>
</g>
<rect x="262.4" y="222" width="9.6" height="22" fill="#7c5cd6">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#7c5cd6;#7f5fd7;#8669da;#9277de;#a18ae3;#b29ee8;#c3b3ee;#d2c5f3;#ded4f7;#e5ddfa;#e8e0fb;#e5ddfa;#ded4f7;#d2c5f3;#c3b3ee;#b29ee8;#a18ae3;#9277de;#8669da;#7f5fd7;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6;#7c5cd6" repeatCount="indefinite"/>
</rect>
<rect x="272" y="222" width="9.6" height="22" fill="#8060d9">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#8060d9;#8060d9;#8060d9;#8262da;#886adc;#9377df;#a188e4;#b19ce9;#c1b0ee;#d0c2f3;#dcd2f7;#e5dcfa;#e8e1fb;#e7dffb;#e0d7f9;#d6caf5;#c8b8f1;#b8a5eb;#a891e6;#997ee1;#8c6fdd;#8465da;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9;#8060d9" repeatCount="indefinite"/>
</rect>
<rect x="281.6" y="222" width="9.6" height="22" fill="#8465dd">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8566dd;#8a6cdf;#9378e2;#a088e6;#af9aea;#bfaeef;#cec0f3;#dbd0f7;#e4dbfa;#e8e1fb;#e8e0fb;#e3dafa;#d9cef7;#ccbef3;#bdacee;#ae98ea;#9f86e5;#9276e1;#896bdf;#8466dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd;#8465dd" repeatCount="indefinite"/>
</rect>
<rect x="291.2" y="222" width="9.6" height="22" fill="#8869e0">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8c6ee1;#9478e4;#a087e7;#ae99eb;#beacef;#ccbef4;#d9cef7;#e3dafa;#e8e0fc;#e9e1fc;#e5dcfb;#ddd2f8;#d1c3f5;#c3b2f1;#b49fec;#a58de8;#987de5;#8f71e2;#896ae0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0;#8869e0" repeatCount="indefinite"/>
</rect>
<rect x="300.8" y="222" width="9.6" height="22" fill="#8c6ee4">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8f72e5;#967ae7;#a188e9;#ae98ed;#bcaaf1;#cbbcf4;#d8ccf8;#e2d8fa;#e8e0fc;#eae2fc;#e7dffc;#e0d6fa;#d5c9f7;#c8b8f3;#b9a6f0;#ab94ec;#9e84e9;#9478e6;#8e70e4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4;#8c6ee4" repeatCount="indefinite"/>
</rect>
<rect x="310.4" y="222" width="9.6" height="22" fill="#9072e7">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9274e7;#987ce9;#a188eb;#ae97ee;#bba8f1;#c9baf5;#d6caf8;#e0d7fa;#e7dffc;#eae2fd;#e8e0fc;#e2d9fb;#d9cdf8;#ccbdf5;#bfacf2;#b19bef;#a48bec;#9a7ee9;#9375e8;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7;#9072e7" repeatCount="indefinite"/>
</rect>
<rect x="320" y="222" width="9.6" height="22" fill="#9577eb">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9678eb;#9b7eec;#a389ee;#ae97f0;#bba7f3;#c9b8f6;#d5c8f9;#e0d5fb;#e7defc;#eae3fd;#eae2fd;#e5dcfc;#dcd1fa;#d1c3f8;#c4b3f5;#b7a2f2;#aa92f0;#a085ed;#997cec;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb;#9577eb" repeatCount="indefinite"/>
</rect>
<rect x="329.6" y="222" width="9.6" height="22" fill="#997bee">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997cee;#9d80ef;#a48af0;#af97f2;#bba6f4;#c8b6f7;#d4c6f9;#ded3fb;#e6ddfc;#eae3fd;#ebe3fd;#e7defd;#dfd5fb;#d5c8f9;#c9b8f7;#bca8f5;#b098f2;#a68bf0;#9e81ef;#9a7cee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee;#997bee" repeatCount="indefinite"/>
</rect>
<rect x="339.2" y="222" width="9.6" height="22" fill="#9d80f1">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#a084f1;#a68cf2;#af97f4;#bba6f6;#c7b5f8;#d3c5fa;#ddd2fb;#e5dcfd;#eae2fd;#ebe4fe;#e8e0fd;#e2d8fc;#d9ccfb;#cebef9;#c1aef7;#b59ff5;#ab92f3;#a388f2;#9e81f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1;#9d80f1" repeatCount="indefinite"/>
</rect>
<rect x="348.8" y="222" width="9.6" height="22" fill="#a184f5">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a386f5;#a88df6;#b098f7;#bba5f8;#c6b4fa;#d2c3fb;#dcd0fc;#e5dbfd;#eae2fe;#ece4fe;#eae2fe;#e5dbfd;#dcd0fc;#d2c3fb;#c6b4fa;#bba5f8;#b098f7;#a88df6;#a386f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5;#a184f5" repeatCount="indefinite"/>
</rect>
<rect x="358.4" y="222" width="9.6" height="22" fill="#a589f8">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a68af8;#aa90f8;#b299f9;#bba6fa;#c6b4fb;#d1c2fc;#dbcffd;#e4dafe;#eae1fe;#ece5fe;#ebe3fe;#e7defe;#dfd4fd;#d6c8fc;#cbbafb;#c0acfa;#b69ff9;#ad94f9;#a88cf8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8;#a589f8" repeatCount="indefinite"/>
</rect>
<rect x="368" y="222" width="9.6" height="22" fill="#a98dfa">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#aa8efa;#ad92fa;#b39afb;#bca6fb;#c6b3fc;#d1c1fd;#dbcefd;#e3d9fe;#e9e1fe;#ece5fe;#ece5fe;#e9e0fe;#e2d8fe;#daccfd;#cfbffd;#c5b1fc;#bba4fb;#b299fb;#ac91fa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa;#a98dfa" repeatCount="indefinite"/>
</rect>
<rect x="377.6" y="222" width="9.6" height="22" fill="#ae91fb">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#b195fb;#b69cfb;#bea6fc;#c7b3fc;#d1c0fd;#dacdfe;#e3d8fe;#e9e0fe;#ece5ff;#ede5ff;#eae2fe;#e5dbfe;#ddd0fe;#d4c4fd;#cab7fd;#c1aafc;#b89ffc;#b297fb;#af92fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb;#ae91fb" repeatCount="indefinite"/>
</rect>
<rect x="387.2" y="222" width="9.6" height="22" fill="#b395fb">
<animate attributeName="fill" calcMode="discrete" dur="3.48s" values="#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b597fb;#b99efb;#c0a7fc;#c8b2fc;#d1bffd;#dacbfd;#e2d6fe;#e8dffe;#ece4ff;#ede6ff;#ece4fe;#e7ddfe;#e1d4fe;#d8c9fd;#cfbcfd;#c6b0fc;#bea5fc;#b89cfb;#b497fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb;#b395fb" repeatCount="indefinite"/>
</rect>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+4 -4
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="832" height="386" viewBox="0 0 832 386" style="max-width: 832px; width: 100%; height: auto;" role="img">
<rect width="100%" height="100%" rx="14" fill="#0c0b14"/>
<rect x="0.5" y="0.5" width="831" height="385" rx="14" fill="none" stroke="#272430" stroke-width="1"/>
<rect width="100%" height="100%" rx="14" fill="#0a0810"/>
<rect x="0.5" y="0.5" width="831" height="385" rx="14" fill="none" stroke="#2d2b31" stroke-width="1"/>
<text x="416" y="23" text-anchor="middle" font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="13" fill="#8a8a8a">torlink</text>
<g font-family='ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace' font-size="16" fill="#ddd8ea">
<text x="339.2" y="84" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#5ae87a" font-weight="600">𐓏</text>
@@ -46,8 +46,8 @@
<rect x="122.5" y="266" width="1.4" height="22" fill="#a78bfa"/>
<text x="137.6" y="282" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#a78bfa"></text>
<rect x="156.8" y="266" width="9.6" height="22" fill="#ddd8ea"/>
<text x="156.8" y="282" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#0c0b14">S</text>
<text x="166.4" y="282" textLength="240" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">earch, or paste a magnet…</text>
<text x="156.8" y="282" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#0a0810">S</text>
<text x="166.4" y="282" textLength="278.4" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill-opacity="0.55">earch or paste a magnet link</text>
<rect x="708.1" y="266" width="1.4" height="22" fill="#a78bfa"/>
<text x="118.4" y="304" textLength="595.2" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#a78bfa">╰────────────────────────────────────────────────────────────╯</text>
<text x="233.6" y="348" textLength="9.6" lengthAdjust="spacingAndGlyphs" xml:space="preserve" fill="#b9a7e6"></text>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+74 -3
View File
@@ -1,8 +1,20 @@
import { lerpHex } from "../src/ui/theme";
import {
SHEEN_PEAK,
SHEEN_TICK_MS,
sheenCenter,
sheenIntensity,
sheenLoopTicks,
sheenPeriod,
} from "../src/ui/sheen";
export interface AnsiToSvgOptions {
cols: number;
bg?: string;
title?: string;
maxWidth?: number;
/** Animate a sweeping highlight over progress-bar fills (rows with a ░ track). */
shimmer?: boolean;
}
interface Style {
@@ -26,7 +38,7 @@ const LINE_H = 22;
const PAD = 32;
const HEADER_H = 36;
const RADIUS = 14;
const BG = "#0c0b14";
const BG = "#0a0810";
const FG_DEFAULT = "#ddd8ea";
const DIM_OPACITY = 0.55;
const FONT_STACK =
@@ -144,7 +156,7 @@ function escapeXml(s: string): string {
const fmt = (n: number): string => String(Math.round(n * 100) / 100);
export function ansiToSvg(frame: string, opts: AnsiToSvgOptions): string {
const { cols, bg = BG, title, maxWidth } = opts;
const { cols, bg = BG, title, maxWidth, shimmer = false } = opts;
const lines = frame.replace(/\r/g, "").split("\n");
const isBlank = (l: string): boolean =>
l.replace(/\x1b\[[0-9;]*m/g, "").trim() === "";
@@ -162,7 +174,7 @@ export function ansiToSvg(frame: string, opts: AnsiToSvgOptions): string {
);
out.push(` <rect width="100%" height="100%" rx="${RADIUS}" fill="${bg}"/>`);
out.push(
` <rect x="0.5" y="0.5" width="${fmt(width - 1)}" height="${fmt(height - 1)}" rx="${RADIUS}" fill="none" stroke="#272430" stroke-width="1"/>`,
` <rect x="0.5" y="0.5" width="${fmt(width - 1)}" height="${fmt(height - 1)}" rx="${RADIUS}" fill="none" stroke="#2d2b31" stroke-width="1"/>`,
);
if (title) {
out.push(
@@ -173,6 +185,9 @@ export function ansiToSvg(frame: string, opts: AnsiToSvgOptions): string {
` <g font-family='${FONT_STACK}' font-size="${FONT_SIZE}" fill="${FG_DEFAULT}">`,
);
const barRuns: { row: number; col: number; n: number; fg: string }[] = [];
const trackLen = new Map<number, number>();
const state = { style: freshStyle() };
lines.forEach((line, row) => {
const runs = parseLine(line, state, cols);
@@ -195,6 +210,11 @@ export function ansiToSvg(frame: string, opts: AnsiToSvgOptions): string {
const x = PAD + col * CHAR_W;
const w = n * CHAR_W;
const fg = st.fg ?? FG_DEFAULT;
if (shimmer) {
if (/^█+$/.test(text)) barRuns.push({ row, col, n, fg });
const dim = (text.match(/░/g) ?? []).length;
if (dim > 0) trackLen.set(row, (trackLen.get(row) ?? 0) + dim);
}
if (/^[│┃]+$/.test(text)) {
const barW = text[0] === "┃" ? 2.8 : 1.4;
for (let k = 0; k < n; k++) {
@@ -251,6 +271,57 @@ export function ansiToSvg(frame: string, opts: AnsiToSvgOptions): string {
});
out.push(" </g>");
if (shimmer && barRuns.length > 0) {
// Reproduce the live ProgressBar sheen 1:1: for each filled cell, animate
// its fill through the exact bell-tinted colors, snapping frame-to-frame
// (calcMode="discrete") like the terminal repaint, so it stays pixelated.
const byRow = new Map<number, { col: number; n: number; fg: string }[]>();
for (const r of barRuns) {
if (!trackLen.has(r.row)) continue;
const arr = byRow.get(r.row) ?? [];
arr.push({ col: r.col, n: r.n, fg: r.fg });
byRow.set(r.row, arr);
}
for (const [row, parts] of byRow) {
parts.sort((a, b) => a.col - b.col);
// Merge contiguous █ runs into one region, expanded to per-cell colors.
const regions: { col: number; colors: string[] }[] = [];
for (const p of parts) {
const last = regions[regions.length - 1];
const cells = Array.from({ length: p.n }, () => p.fg);
if (last && last.col + last.colors.length === p.col) last.colors.push(...cells);
else regions.push({ col: p.col, colors: cells });
}
const yTop = HEADER_H + PAD + row * LINE_H;
for (const region of regions) {
const len = region.colors.length;
const fullCells = len + (trackLen.get(row) ?? 0);
const period = sheenPeriod(fullCells);
const frames = sheenLoopTicks(period);
const dur = (frames * SHEEN_TICK_MS) / 1000;
for (let i = 0; i < len; i++) {
const base = region.colors[i]!;
const values: string[] = [];
for (let t = 0; t < frames; t++) {
const intensity = sheenIntensity(i, sheenCenter(t, period));
values.push(intensity > 0 ? lerpHex(base, SHEEN_PEAK, intensity) : base);
}
const x = PAD + (region.col + i) * CHAR_W;
out.push(
` <rect x="${fmt(x)}" y="${fmt(yTop)}" width="${fmt(CHAR_W)}" height="${LINE_H}" fill="${base}">`,
);
out.push(
` <animate attributeName="fill" calcMode="discrete" dur="${fmt(dur)}s" values="${values.join(";")}" repeatCount="indefinite"/>`,
);
out.push(` </rect>`);
}
}
}
}
out.push("</svg>");
return out.join("\n");
}
+14 -5
View File
@@ -16,7 +16,7 @@ import { Downloads } from "../src/ui/components/Downloads";
import { footerHints } from "../src/ui/keymap";
import { sourcesByGroup } from "../src/sources/registry";
import { cleanText, formatBytes, formatRelative } from "../src/util/format";
import { ansiToSvg } from "./ansi-to-svg";
import { ansiToSvg, type AnsiToSvgOptions } from "./ansi-to-svg";
import type { Config } from "../src/config/config";
import type { DownloadQueue } from "../src/download/queue";
import type { QueueItem } from "../src/download/types";
@@ -97,7 +97,12 @@ function makeStore(
};
}
function save(name: string, store: Store, node: React.ReactNode): void {
function save(
name: string,
store: Store,
node: React.ReactNode,
extra: Partial<AnsiToSvgOptions> = {},
): void {
const { lastFrame, unmount } = render(
<StoreContext.Provider value={store}>{node}</StoreContext.Provider>,
);
@@ -106,7 +111,10 @@ function save(name: string, store: Store, node: React.ReactNode): void {
if (!/\x1b\[/.test(frame)) {
throw new Error(`${name}: frame has no ANSI colors (FORCE_COLOR didn't take)`);
}
writeFileSync(join(OUT_DIR, `${name}.svg`), ansiToSvg(frame, { cols: COLS, title: "torlink" }));
writeFileSync(
join(OUT_DIR, `${name}.svg`),
ansiToSvg(frame, { cols: COLS, title: "torlink", ...extra }),
);
console.log(`preview/${name}.svg`);
}
@@ -126,7 +134,7 @@ save(
<Text dimColor>{CATEGORIES}</Text>
</Box>
<Box marginTop={1} width={62}>
<SearchBar width={62} value="" editing placeholder="Search, or paste a magnet…" onSubmit={() => {}} />
<SearchBar width={62} value="" editing placeholder="Search or paste a magnet link…" onSubmit={() => {}} />
</Box>
<Box marginTop={1}>
<Text>
@@ -159,7 +167,7 @@ save(
<Box height={14} marginTop={1}>
<Sidebar />
<Box flexGrow={1} flexDirection="column">
<SearchBar width={CONTENT_WIDTH} value="" editing={false} placeholder="Search, or paste a magnet…" onSubmit={() => {}} />
<SearchBar width={CONTENT_WIDTH} value="" editing={false} placeholder="Search or paste a magnet link…" onSubmit={() => {}} />
<Box marginTop={1}>
<Panel title="latest" width={CONTENT_WIDTH} focused count={`(${browseResults.length})`} height={9}>
<Box><Text dimColor>newest across all sources</Text></Box>
@@ -237,4 +245,5 @@ save(
</Box>
<Footer hints={footerHints("content", "downloads")} />
</Box>,
{ shimmer: true },
);
+6 -13
View File
@@ -1,13 +1,9 @@
import { useEffect, useState } from "react";
import { Text } from "ink";
import { COLOR, RULE, lerpHex } from "../theme";
import { SHEEN_PEAK, SHEEN_TICK_MS, sheenCenter, sheenIntensity, sheenPeriod } from "../sheen";
const DEEP = "#7c5cd6";
const SHEEN_PEAK = "#f4efff";
const RADIUS = 3.5;
const GAP = 8;
const MAX_SHEEN = 0.9;
const TICK_MS = 90;
interface Run {
color: string;
@@ -55,7 +51,7 @@ export function ProgressBar({
const [tick, setTick] = useState(0);
useEffect(() => {
if (!animate) return;
const timer = setInterval(() => setTick((v) => v + 1), TICK_MS);
const timer = setInterval(() => setTick((v) => v + 1), SHEEN_TICK_MS);
timer.unref?.();
return () => clearInterval(timer);
}, [animate]);
@@ -76,15 +72,12 @@ export function ProgressBar({
);
}
const period = Math.ceil(width + RADIUS * 2) + GAP;
const center = (tick % period) - RADIUS;
const period = sheenPeriod(width);
const center = sheenCenter(tick, period);
const cells = Array.from({ length: filled }, (_, i) => {
let c = ramp(i / denom, DEEP, COLOR.accent, COLOR.bright);
const d = Math.abs(i - center);
if (d < RADIUS) {
const intensity = 0.5 * (1 + Math.cos((Math.PI * d) / RADIUS)) * MAX_SHEEN;
c = lerpHex(c, SHEEN_PEAK, intensity);
}
const intensity = sheenIntensity(i, center);
if (intensity > 0) c = lerpHex(c, SHEEN_PEAK, intensity);
return c;
});
+1 -1
View File
@@ -14,7 +14,7 @@ import type { Source, TorrentResult } from "../../sources/types";
type Mode = "list" | "search" | "detail";
const PLACEHOLDER = "Search, or paste a magnet…";
const PLACEHOLDER = "Search or paste a magnet link…";
function DetailRow({ label, value }: { label: string; value: ReactNode }) {
return (
+37
View File
@@ -0,0 +1,37 @@
// Single source of truth for the animated progress-bar sheen, shared by the
// live TUI (src/ui/components/ProgressBar.tsx) and the README SVG generator
// (scripts/ansi-to-svg.ts) so the preview renders 1:1 with the terminal.
//
// The highlight is a cosine bell that sweeps across the filled cells. Position
// advances by a fractional SHEEN_SPEED per tick at a fast SHEEN_TICK_MS, so the
// peak glides between cells (intensity-interpolated) instead of stepping a whole
// cell at a time, while the cells themselves stay discrete (pixelated).
export const SHEEN_PEAK = "#f4efff";
export const SHEEN_RADIUS = 4.5; // bell half-width, in cells
export const SHEEN_GAP = 8; // dark cells between sweeps
export const SHEEN_TICK_MS = 40; // frame interval (~25fps)
export const SHEEN_SPEED = 0.45; // cells advanced per tick (~11 cells/sec)
export const SHEEN_MAX = 0.9; // peak blend toward SHEEN_PEAK
/** Total sweep cycle length in cells, for a bar `width` cells wide. */
export function sheenPeriod(width: number): number {
return Math.ceil(width + SHEEN_RADIUS * 2) + SHEEN_GAP;
}
/** Fractional center of the bell at a given tick (loops over `period`). */
export function sheenCenter(tick: number, period: number): number {
return ((tick * SHEEN_SPEED) % period) - SHEEN_RADIUS;
}
/** Sheen intensity (0..SHEEN_MAX) for cell `i` given the bell `center`. */
export function sheenIntensity(i: number, center: number): number {
const d = Math.abs(i - center);
if (d >= SHEEN_RADIUS) return 0;
return 0.5 * (1 + Math.cos((Math.PI * d) / SHEEN_RADIUS)) * SHEEN_MAX;
}
/** Ticks in one full visual loop, for sampling the SVG flipbook 1:1. */
export function sheenLoopTicks(period: number): number {
return Math.round(period / SHEEN_SPEED);
}
+1 -1
View File
@@ -50,7 +50,7 @@ export function Splash() {
width={barWidth}
value=""
editing
placeholder="Search, or paste a magnet…"
placeholder="Search or paste a magnet link…"
onSubmit={submitQuery}
/>
</Box>