diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b817f1b..66c49ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,11 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - - run: cargo build --verbose - - run: cargo test --verbose + # chat-cli's build.rs unconditionally links liblogosdelivery and requires + # LOGOS_DELIVERY_LIB_DIR. The smoketest job builds and exercises it under + # Nix; here we keep the toolchain-only job fast by skipping it. + - run: cargo build --verbose --workspace --exclude chat-cli + - run: cargo test --verbose --workspace --exclude chat-cli clippy: name: Clippy @@ -26,7 +29,7 @@ jobs: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - run: rustup component add clippy - - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo clippy --all-targets --all-features --workspace --exclude chat-cli -- -D warnings fmt: name: Format diff --git a/README.md b/README.md index 186e1ed..502fd95 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,17 @@ Supporting library for Logos-chat ## Example app [`bin/chat-cli`](bin/chat-cli/) is an end-to-end encrypted CLI chat app -built on this library. It uses [logos-delivery](https://github.com/logos-messaging/logos-delivery) +built on this library. By default it uses [logos-delivery](https://github.com/logos-messaging/logos-delivery) (Waku-based) as the transport so two users anywhere in the world can chat by -sharing an intro bundle. +sharing an intro bundle. A local file transport is also bundled in; pick at +runtime with `--transport `. ```sh # Build logos-delivery with Nix nix build .#logos-delivery # Build chat-cli with Cargo LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli -# Run binary +# Run binary (defaults to --transport logos-delivery) ./target/release/chat-cli --name alice ``` diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index 8a7b784..615992a 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -21,6 +21,3 @@ base64 = "0.22" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(logos_delivery)'] } diff --git a/bin/chat-cli/README.md b/bin/chat-cli/README.md index 470b16b..217400f 100644 --- a/bin/chat-cli/README.md +++ b/bin/chat-cli/README.md @@ -4,8 +4,6 @@ A terminal chat application built on top of libchat. End-to-end encrypted messag ## Building -### With logos-delivery transport (recommended) - [logos-delivery](https://github.com/logos-messaging/logos-delivery) is exposed as a Nix package. Build it once, then point `LOGOS_DELIVERY_LIB_DIR` at the result: @@ -16,31 +14,35 @@ LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli The binary lands at `target/release/chat-cli`. -### File transport only (no Nix required) - -```bash -cargo build --release -p chat-cli -``` - ## Transports -| Transport | Description | -|-----------|-------------| -| File (default) | Shared directory; no network needed — great for local testing | -| logos-delivery | Embedded Waku node on the logos.dev network | +Both transports are compiled into the binary and selected at runtime via `--transport`: -The transport is selected automatically at compile time: if `LOGOS_DELIVERY_LIB_DIR` is set when building, logos-delivery is used; otherwise the file transport is used. +| Value (`--transport`) | Description | +|-----------------------|-------------| +| `logos-delivery` (default) | Embedded Waku node on the logos.dev network | +| `file` | Shared directory; no network needed — great for local testing | -## Quick start (file transport) +## Quick start -Run two instances in separate terminals, pointing at the same data directory: +Run two instances in separate terminals: ```bash # Terminal 1 -cargo run -p chat-cli -- --name alice +cargo run -p chat-cli -- --name alice --port 60001 # Terminal 2 -cargo run -p chat-cli -- --name bob +cargo run -p chat-cli -- --name bob --port 60002 +``` + +For local-only testing without any network dependency, use the file transport: + +```bash +# Terminal 1 +cargo run -p chat-cli -- --name alice --transport file + +# Terminal 2 +cargo run -p chat-cli -- --name bob --transport file ``` ### Establishing a connection @@ -49,20 +51,14 @@ cargo run -p chat-cli -- --name bob 2. In Bob's terminal, type `/connect `. 3. Bob's "Hello!" message appears in Alice's terminal. Both can now chat. -## logos-delivery transport - -After building with `LOGOS_DELIVERY_LIB_DIR` set, run: - -```bash -./target/release/chat-cli --name alice -``` - -Optional flags: +## Options | Flag | Default | Description | |------|---------|-------------| -| `--db ` | *(ephemeral)* | SQLite file for persistent identity across restarts | -| `--preset ` | `logos.dev` | Network preset (`logos.dev` or `twn`) | +| `--transport ` | `logos-delivery` | Transport to use (`logos-delivery` or `file`) | +| `--data ` | `tmp/chat-cli-data` | Data directory (UI state and default SQLite path) | +| `--db ` | `/.db` | SQLite file for persistent identity | +| `--preset ` | `logos.dev` | logos-delivery network preset | | `--port ` | `60000` | TCP port for the embedded logos-delivery node | | `--log-file ` | *(stderr, off)* | Write logs to a file instead of stderr | @@ -80,7 +76,7 @@ Optional flags: | `/clear` | Clear current chat's message history | | `/quit` · `Esc` · `Ctrl+C` | Exit | -## Storage (file transport) +## Storage All data lives under `tmp/chat-cli-data/` by default (override with `--data`): @@ -88,7 +84,7 @@ All data lives under `tmp/chat-cli-data/` by default (override with `--data`): |------|----------| | `.db` | SQLite — identity keys, ratchet state, chat metadata (encrypted) | | `_state.json` | UI state — message history, active chat | -| `transport//` | Inbox directory watched for incoming messages | +| `transport//` | Inbox directory watched for incoming messages (file transport only) | The SQLite database can be inspected with *DB Browser for SQLite*: password `chat-cli`, cipher `SQLCipher 4 defaults`. @@ -97,7 +93,7 @@ The SQLite database can be inspected with *DB Browser for SQLite*: password `cha ``` bin/chat-cli/ ├── src/ -│ ├── main.rs entry point, CLI arg parsing, transport selection +│ ├── main.rs entry point, CLI arg parsing, runtime transport dispatch │ ├── app.rs application state and command handling │ ├── ui.rs ratatui terminal UI │ ├── utils.rs shared helpers @@ -105,5 +101,5 @@ bin/chat-cli/ │ └── transport/ │ ├── file.rs file-based transport │ └── logos_delivery.rs logos-delivery (Waku) transport + FFI -└── build.rs links liblogosdelivery when LOGOS_DELIVERY_LIB_DIR is set +└── build.rs links liblogosdelivery (LOGOS_DELIVERY_LIB_DIR required) ``` diff --git a/bin/chat-cli/build.rs b/bin/chat-cli/build.rs index a7fa686..fec31ae 100644 --- a/bin/chat-cli/build.rs +++ b/bin/chat-cli/build.rs @@ -1,20 +1,17 @@ fn main() { - println!("cargo::rustc-check-cfg=cfg(logos_delivery)"); println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR"); - let Ok(lib_dir) = std::env::var("LOGOS_DELIVERY_LIB_DIR") else { - return; - }; + let lib_dir = std::env::var("LOGOS_DELIVERY_LIB_DIR").expect( + "LOGOS_DELIVERY_LIB_DIR must be set; build liblogosdelivery via \ + `nix build .#logos-delivery` and point this var at the result/lib directory", + ); - println!("cargo:rustc-cfg=logos_delivery"); println!("cargo:rustc-link-search=native={lib_dir}"); println!("cargo:rustc-link-lib=dylib=logosdelivery"); - // Set rpath so the binary finds the shared library at runtime. let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); match target_os.as_str() { - "macos" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"), - "linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"), + "macos" | "linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}"), other => panic!("unsupported OS for logos-delivery transport: {other}"), } } diff --git a/bin/chat-cli/src/main.rs b/bin/chat-cli/src/main.rs index 0326330..6b5928f 100644 --- a/bin/chat-cli/src/main.rs +++ b/bin/chat-cli/src/main.rs @@ -3,14 +3,22 @@ mod transport; mod ui; mod utils; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::sync::mpsc; use anyhow::{Context, Result}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use client::DeliveryService; use app::ChatApp; +#[derive(Copy, Clone, Debug, ValueEnum)] +#[value(rename_all = "kebab-case")] +enum TransportKind { + File, + LogosDelivery, +} + #[derive(Parser, Debug)] #[command(name = "chat-cli", about = "End-to-end encrypted terminal chat")] struct Cli { @@ -18,17 +26,20 @@ struct Cli { #[arg(long, short)] name: String, - // ── File-transport options ──────────────────────────────────────────────── - /// Shared data directory for file transport (both peers must use the same path). + /// Which delivery transport to use. + #[arg(long, value_enum, default_value_t = TransportKind::LogosDelivery)] + transport: TransportKind, + + /// Data directory (used for UI state and the default SQLite path). #[arg(long, default_value = "tmp/chat-cli-data")] data: PathBuf, - // ── logos-delivery transport options ────────────────────────────────────── - /// Persistent SQLite database for logos-delivery transport (omit for ephemeral identity). + /// Override the SQLite database path (defaults to `/.db`). #[arg(long)] db: Option, - /// logos-delivery network preset (`logos.dev` or `twn`). + // ── logos-delivery transport options ────────────────────────────────────── + /// logos-delivery network preset (e.g. `logos.dev`). #[arg(long, default_value = "logos.dev")] preset: String, @@ -48,30 +59,54 @@ struct Cli { fn main() -> Result<()> { let cli = Cli::parse(); setup_logging(cli.log_file.as_deref())?; - #[cfg(logos_delivery)] - return run_logos_delivery(cli); - #[cfg(not(logos_delivery))] - run_file(cli) -} - -#[cfg(not(logos_delivery))] -fn run_file(cli: Cli) -> Result<()> { - use transport::file::FileTransport; std::fs::create_dir_all(&cli.data).context("failed to create data directory")?; - println!("Starting chat as '{}'...", cli.name); - println!("Data dir: {}", cli.data.display()); + match cli.transport { + TransportKind::File => { + let transport_dir = cli.data.join("transport"); + let (transport, inbound) = transport::file::FileTransport::new(&transport_dir) + .context("failed to create file transport")?; + run(transport, inbound, &cli) + } + TransportKind::LogosDelivery => { + use transport::logos_delivery::{Config, Service}; - let transport_dir = cli.data.join("transport"); - let (transport, inbound) = - FileTransport::new(&transport_dir).context("failed to create file transport")?; + eprintln!("Starting logos-delivery node (preset={})...", cli.preset); + eprintln!("This may take a few seconds while connecting to the network."); + + let cfg = Config { + preset: cli.preset.clone(), + tcp_port: cli.port, + ..Default::default() + }; + let (transport, inbound) = + Service::start(cfg).context("failed to start logos-delivery")?; + + eprintln!("Node connected. Initializing chat client..."); + run(transport, inbound, &cli) + } + } +} + +fn run( + transport: D, + inbound: mpsc::Receiver>, + cli: &Cli, +) -> Result<()> { + let db_path = cli + .db + .clone() + .unwrap_or_else(|| cli.data.join(format!("{}.db", cli.name))); + let db_str = db_path + .to_str() + .context("db path contains non-UTF-8 characters")? + .to_string(); - let db_path = cli.data.join(format!("{}.db", cli.name)); let client = client::ChatClient::open( cli.name.clone(), client::StorageConfig::Encrypted { - path: db_path.to_string_lossy().to_string(), + path: db_str, key: "chat-cli".to_string(), }, transport, @@ -91,71 +126,6 @@ fn run_file(cli: Cli) -> Result<()> { result } -#[cfg_attr(not(logos_delivery), allow(dead_code, unused_variables))] -fn run_logos_delivery(cli: Cli) -> Result<()> { - #[cfg(logos_delivery)] - { - use transport::logos_delivery::{Config, Service}; - - eprintln!("Starting logos-delivery node (preset={})...", cli.preset); - eprintln!("This may take a few seconds while connecting to the network."); - - let logos_cfg = Config { - preset: cli.preset.clone(), - tcp_port: cli.port, - ..Default::default() - }; - let (delivery, inbound) = - Service::start(logos_cfg).context("failed to start logos-delivery")?; - - eprintln!("Node connected. Initializing chat client..."); - - let data_dir = cli - .db - .as_ref() - .and_then(|p| p.parent()) - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| cli.data.clone()); - - let client = match cli.db { - Some(ref path) => { - let db_str = path - .to_str() - .context("db path contains non-UTF-8 characters")? - .to_string(); - client::ChatClient::open( - cli.name.clone(), - client::StorageConfig::Encrypted { - path: db_str, - key: "chat-cli".to_string(), - }, - delivery, - ) - .map_err(|e| anyhow::anyhow!("{e:?}")) - .context("failed to open persistent client")? - } - None => client::ChatClient::new(cli.name.clone(), delivery), - }; - - let mut app = ChatApp::new(client, inbound, &cli.name, &data_dir)?; - - if cli.smoketest { - return Ok(()); - } - - let mut terminal = ui::init().context("failed to initialize terminal")?; - let result = run_app(&mut terminal, &mut app); - ui::restore().context("failed to restore terminal")?; - return result; - } - - #[cfg(not(logos_delivery))] - anyhow::bail!( - "logos-delivery transport is not available in this build.\n\ - Build with LOGOS_DELIVERY_LIB_DIR set to enable it." - ) -} - fn run_app(terminal: &mut ui::Tui, app: &mut ChatApp) -> Result<()> { loop { app.process_incoming()?; @@ -167,7 +137,7 @@ fn run_app(terminal: &mut ui::Tui, app: &mut ChatApp) -> Ok(()) } -fn setup_logging(log_file: Option<&std::path::Path>) -> Result<()> { +fn setup_logging(log_file: Option<&Path>) -> Result<()> { use tracing_subscriber::EnvFilter; let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")); diff --git a/bin/chat-cli/src/transport.rs b/bin/chat-cli/src/transport.rs index 625f9e2..2773d1e 100644 --- a/bin/chat-cli/src/transport.rs +++ b/bin/chat-cli/src/transport.rs @@ -1,4 +1,2 @@ -#[cfg(not(logos_delivery))] pub mod file; -#[cfg(logos_delivery)] pub mod logos_delivery; diff --git a/flake.nix b/flake.nix index ca1b52e..e588d48 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ]; buildType = "release"; doCheck = false; + cargoBuildFlags = [ "--workspace" "--exclude" "chat-cli" ]; postBuild = '' cargo run --frozen --release --bin generate-headers --features headers -p client-ffi -- crates/client-ffi/client_ffi.h