foxygit / Torlinkc Log in
commit 2e7e38941e084d8350e160a37a63326502113190
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Sun Aug 23 09:12:27 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Sun Aug 23 09:12:27 2026 +0200

    Phase 0: toolchain proof for libtorrent-rasterbar + FTXUI

    C++ rewrite of torlink (see the TS project's history/README for context).
    Chosen stack: libtorrent-rasterbar for the torrent engine, FTXUI for the
    terminal UI, both available as Debian dev packages with CMake config files
    so no vcpkg bootstrap is needed on Linux.

    Two smoke tests prove the toolchain end-to-end before any real porting
    starts: libtorrent_hello adds a public-domain magnet and streams alert-based
    progress; ftxui_hello renders a bordered ScreenInteractive::Fullscreen()
    component tree. Both build, run, and shut down cleanly, and are wired into
    CI.
---
 .github/workflows/ci.yml    | 58 +++++++++++++++++++++++++++++
 .gitignore                  |  1 +
 CMakeLists.txt              | 16 ++++++++
 phase0/CMakeLists.txt       |  5 +++
 phase0/ftxui_hello.cpp      | 49 ++++++++++++++++++++++++
 phase0/libtorrent_hello.cpp | 91 +++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 220 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8ce06b1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,58 @@
+name: CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+
+permissions:
+  contents: read
+
+# A new push to the same branch supersedes the run in flight.
+concurrency:
+  group: ci-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  build:
+    # Linux only for now: the rewrite targets Linux/macOS first and Windows
+    # is explicitly deferred (no daemonize/attach equivalent yet). macOS is
+    # meant to follow once Phase 0 is proven here, not day one.
+    name: build & smoke test (ubuntu)
+    runs-on: ubuntu-latest
+    timeout-minutes: 15
+
+    steps:
+      - uses: actions/checkout@v7
+
+      # libtorrent-rasterbar and ftxui both ship dev packages + CMake config
+      # files directly in Debian/Ubuntu, so no vcpkg bootstrap is needed.
+      - name: Install toolchain and libraries
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends \
+            cmake build-essential pkgconf \
+            libtorrent-rasterbar-dev libftxui-dev \
+            libboost-system-dev libssl-dev
+
+      - name: Configure
+        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
+
+      - name: Build
+        run: cmake --build build -j"$(nproc)"
+
+      - name: Run test suite
+        working-directory: build
+        run: ctest --output-on-failure
+
+      # Phase 0 smoke tests: prove the toolchain actually runs, not just links.
+      # libtorrent_hello needs no network guarantee to pass (peers may or may
+      # not be reachable from a CI runner) — it only has to start, poll
+      # alerts, and shut down cleanly within the timeout.
+      - name: Smoke test libtorrent hello-world
+        run: timeout 20 ./build/phase0/libtorrent_hello
+
+      # No real TTY in CI, so ftxui_hello self-exits via this env var instead
+      # of waiting on a keypress.
+      - name: Smoke test FTXUI hello-world
+        run: TORLINK_PHASE0_AUTOEXIT=1 timeout 10 ./build/phase0/ftxui_hello < /dev/null
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..567609b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..6b09b22
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,16 @@
+cmake_minimum_required(VERSION 3.20)
+project(torlinkc CXX)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+if(NOT CMAKE_BUILD_TYPE)
+  set(CMAKE_BUILD_TYPE RelWithDebInfo)
+endif()
+
+find_package(LibtorrentRasterbar REQUIRED)
+find_package(ftxui REQUIRED)
+
+enable_testing()
+
+add_subdirectory(phase0)
diff --git a/phase0/CMakeLists.txt b/phase0/CMakeLists.txt
new file mode 100644
index 0000000..96b5f42
--- /dev/null
+++ b/phase0/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_executable(libtorrent_hello libtorrent_hello.cpp)
+target_link_libraries(libtorrent_hello PRIVATE LibtorrentRasterbar::torrent-rasterbar)
+
+add_executable(ftxui_hello ftxui_hello.cpp)
+target_link_libraries(ftxui_hello PRIVATE ftxui::component ftxui::dom ftxui::screen)
diff --git a/phase0/ftxui_hello.cpp b/phase0/ftxui_hello.cpp
new file mode 100644
index 0000000..624bc18
--- /dev/null
+++ b/phase0/ftxui_hello.cpp
@@ -0,0 +1,49 @@
+// Phase 0 toolchain proof: an FTXUI ScreenInteractive::Fullscreen() component
+// tree, quit on q/esc. TORLINK_PHASE0_AUTOEXIT lets this run unattended as a
+// smoke test (no real TTY / CI) by exiting itself after 2 seconds.
+
+#include <ftxui/component/component.hpp>
+#include <ftxui/component/screen_interactive.hpp>
+#include <ftxui/dom/elements.hpp>
+
+#include <chrono>
+#include <cstdlib>
+#include <thread>
+
+using namespace ftxui;
+
+int main() {
+  auto screen = ScreenInteractive::Fullscreen();
+
+  auto renderer = Renderer([&] {
+    return vbox({
+               text("torlink phase 0 - FTXUI hello") | bold | center,
+               separator(),
+               text("press q or esc to quit") | dim | center,
+           }) |
+           border | center;
+  });
+
+  auto component = CatchEvent(renderer, [&](Event event) {
+    if (event == Event::Character('q') || event == Event::Escape) {
+      screen.Exit();
+      return true;
+    }
+    return false;
+  });
+
+  std::thread auto_exit_timer;
+  if (std::getenv("TORLINK_PHASE0_AUTOEXIT") != nullptr) {
+    auto_exit_timer = std::thread([&] {
+      std::this_thread::sleep_for(std::chrono::seconds(2));
+      screen.Exit();
+    });
+  }
+
+  screen.Loop(component);
+
+  if (auto_exit_timer.joinable()) {
+    auto_exit_timer.join();
+  }
+  return 0;
+}
diff --git a/phase0/libtorrent_hello.cpp b/phase0/libtorrent_hello.cpp
new file mode 100644
index 0000000..25998b8
--- /dev/null
+++ b/phase0/libtorrent_hello.cpp
@@ -0,0 +1,91 @@
+// Phase 0 toolchain proof: open an lt::session, add a well-known public-domain
+// magnet (Sintel, the Blender Foundation's CC-licensed short film, commonly
+// used as a test torrent), poll alerts for progress, and shut down cleanly.
+
+#include <libtorrent/alert_types.hpp>
+#include <libtorrent/add_torrent_params.hpp>
+#include <libtorrent/magnet_uri.hpp>
+#include <libtorrent/session.hpp>
+#include <libtorrent/settings_pack.hpp>
+#include <libtorrent/torrent_status.hpp>
+
+#include <chrono>
+#include <cstdlib>
+#include <filesystem>
+#include <iostream>
+#include <thread>
+
+namespace lt = libtorrent;
+
+namespace {
+constexpr auto kMagnet =
+    "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10"
+    "&dn=Sintel"
+    "&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce"
+    "&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce";
+}
+
+int main() {
+  const auto save_path = std::filesystem::temp_directory_path() / "torlink-cpp-phase0";
+  std::filesystem::create_directories(save_path);
+
+  lt::settings_pack pack;
+  pack.set_int(lt::settings_pack::alert_mask,
+               lt::alert_category::status | lt::alert_category::error);
+
+  lt::session ses(pack);
+
+  lt::error_code ec;
+  lt::add_torrent_params params = lt::parse_magnet_uri(kMagnet, ec);
+  if (ec) {
+    std::cerr << "failed to parse magnet: " << ec.message() << "\n";
+    return 1;
+  }
+  params.save_path = save_path.string();
+
+  ses.async_add_torrent(std::move(params));
+
+  std::cout << "session started, listening on port "
+            << ses.listen_port() << "\n";
+  std::cout << "watching '" << kMagnet << "' for 10s (save path: "
+            << save_path << ")\n";
+
+  lt::torrent_handle handle;
+
+  for (int tick = 0; tick < 20; ++tick) {
+    std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+    std::vector<lt::alert*> alerts;
+    ses.pop_alerts(&alerts);
+    for (lt::alert* a : alerts) {
+      if (auto* at = lt::alert_cast<lt::add_torrent_alert>(a)) {
+        handle = at->handle;
+        std::cout << "[alert] torrent added: " << at->handle.status().name
+                  << "\n";
+      } else if (auto* mt = lt::alert_cast<lt::metadata_received_alert>(a)) {
+        std::cout << "[alert] metadata received: "
+                  << mt->handle.status().name << "\n";
+      } else if (auto* st = lt::alert_cast<lt::state_update_alert>(a)) {
+        for (const lt::torrent_status& s : st->status) {
+          std::cout << "[status] progress=" << (s.progress * 100.0) << "% "
+                    << "peers=" << s.num_peers << " "
+                    << "down=" << s.download_rate << "B/s"
+                    << "\n";
+        }
+      } else if (auto* et = lt::alert_cast<lt::torrent_error_alert>(a)) {
+        std::cout << "[alert] torrent error: " << et->message() << "\n";
+      } else if (auto* lf = lt::alert_cast<lt::listen_failed_alert>(a)) {
+        std::cout << "[alert] listen failed: " << lf->message() << "\n";
+      }
+    }
+
+    ses.post_torrent_updates();
+  }
+
+  if (handle.is_valid()) {
+    ses.remove_torrent(handle);
+  }
+
+  std::cout << "shutting down cleanly\n";
+  return 0;
+}