Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package ttl for openSUSE:Factory checked in at 2026-07-09 22:18:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ttl (Old) and /work/SRC/openSUSE:Factory/.ttl.new.1991 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ttl" Thu Jul 9 22:18:58 2026 rev:9 rq:1364590 version:0.21.0 Changes: -------- --- /work/SRC/openSUSE:Factory/ttl/ttl.changes 2026-07-07 21:07:31.003247220 +0200 +++ /work/SRC/openSUSE:Factory/.ttl.new.1991/ttl.changes 2026-07-09 22:19:30.150376648 +0200 @@ -1,0 +2,8 @@ +Wed Jul 8 06:41:35 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 0.21.0: + * feat(update): allow disabling the update check (#110) (#122) +- Build with "--no-default-features" to disable the automatic + update check (update-informer). + +------------------------------------------------------------------- Old: ---- ttl-0.20.2.obscpio New: ---- ttl-0.21.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ttl.spec ++++++ --- /var/tmp/diff_new_pack.D8l3cM/_old 2026-07-09 22:19:31.554424346 +0200 +++ /var/tmp/diff_new_pack.D8l3cM/_new 2026-07-09 22:19:31.558424481 +0200 @@ -17,7 +17,7 @@ Name: ttl -Version: 0.20.2 +Version: 0.21.0 Release: 0 Summary: Modern traceroute/mtr-style TUI License: Apache-2.0 OR MIT @@ -38,7 +38,7 @@ %autosetup -p 1 -a 1 %build -%{cargo_build} +%{cargo_build} --no-default-features %install %{cargo_install} ++++++ _service ++++++ --- /var/tmp/diff_new_pack.D8l3cM/_old 2026-07-09 22:19:31.638427201 +0200 +++ /var/tmp/diff_new_pack.D8l3cM/_new 2026-07-09 22:19:31.650427609 +0200 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/lance0/ttl</param> <param name="scm">git</param> - <param name="revision">v0.20.2</param> + <param name="revision">v0.21.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.D8l3cM/_old 2026-07-09 22:19:31.698429241 +0200 +++ /var/tmp/diff_new_pack.D8l3cM/_new 2026-07-09 22:19:31.706429512 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/lance0/ttl</param> - <param name="changesrevision">11349e34e91844b2f6144ae4e6d412b3f9ae5220</param></service></servicedata> + <param name="changesrevision">46f1dc2cd8c055408a764d5674afa897ca7cd30c</param></service></servicedata> (No newline at EOF) ++++++ ttl-0.20.2.obscpio -> ttl-0.21.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/.github/dependabot.yml new/ttl-0.21.0/.github/dependabot.yml --- old/ttl-0.20.2/.github/dependabot.yml 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/.github/dependabot.yml 2026-07-07 16:42:22.000000000 +0200 @@ -4,7 +4,22 @@ directory: "/" schedule: interval: "weekly" + groups: + cargo-dependencies: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + groups: + github-actions: + patterns: + - "*" + ignore: + # dtolnay/rust-toolchain is pinned by Rust *version*, not action version. + # @1.88 in the MSRV job = our declared MSRV (Cargo.toml rust-version); + # dependabot misreads it as a stale tag and tries to bump it (e.g. #121: + # 1.88 -> 1.100), which would silently defeat the MSRV check. Other uses + # are @stable. MSRV changes are made by hand. + - dependency-name: "dtolnay/rust-toolchain" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/.github/workflows/audit.yml new/ttl-0.21.0/.github/workflows/audit.yml --- old/ttl-0.20.2/.github/workflows/audit.yml 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/.github/workflows/audit.yml 2026-07-07 16:42:22.000000000 +0200 @@ -12,14 +12,21 @@ jobs: audit: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - name: Install Rust uses: dtolnay/rust-toolchain@stable + - name: Cache cargo-audit + uses: actions/cache@v6 + with: + path: ~/.cargo/bin/cargo-audit + key: cargo-audit-0.21 + - name: Install cargo-audit - run: cargo install cargo-audit --locked + run: command -v cargo-audit || cargo install cargo-audit --locked - name: Run security audit run: cargo audit diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/.github/workflows/ci.yml new/ttl-0.21.0/.github/workflows/ci.yml --- old/ttl-0.20.2/.github/workflows/ci.yml 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/.github/workflows/ci.yml 2026-07-07 16:42:22.000000000 +0200 @@ -6,12 +6,17 @@ pull_request: branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 @@ -27,6 +32,11 @@ - name: Run tests run: cargo test --verbose + - name: Check docs + run: cargo doc --locked --no-deps --document-private-items + env: + RUSTDOCFLAGS: -D warnings + # Real-kernel validation of the IP_HDRINCL send path (issue #12 fix): the ignored # test crafts IPv4 ICMP/UDP/TCP packets and sends them via a raw IP_HDRINCL socket, # which only succeeds if this OS's ip_len/ip_off byte order is correct (BSD kernels @@ -39,13 +49,14 @@ sudo "$BIN" runtime_hdrincl --ignored --nocapture - name: Clippy - run: cargo clippy -- -D warnings + run: cargo clippy --all-targets -- -D warnings - name: Format check run: cargo fmt -- --check build-macos: runs-on: macos-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v7 @@ -77,6 +88,7 @@ msrv: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@v7 @@ -87,10 +99,14 @@ uses: Swatinem/rust-cache@v2 - name: Check MSRV - run: cargo check --verbose + run: cargo check --all-targets --locked --verbose + + - name: Test MSRV lib + run: cargo test --lib --locked --verbose build-freebsd: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@v7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/.gitignore new/ttl-0.21.0/.gitignore --- old/ttl-0.20.2/.gitignore 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/.gitignore 2026-07-07 16:42:22.000000000 +0200 @@ -5,3 +5,6 @@ .DS_Store *.rlib .claude/ + +# Code review state +.last_review_sha diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/CHANGELOG.md new/ttl-0.21.0/CHANGELOG.md --- old/ttl-0.20.2/CHANGELOG.md 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/CHANGELOG.md 2026-07-07 16:42:22.000000000 +0200 @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.21.0] - 2026-07-07 + +### Added +- **Opt out of the startup update check** (#110). The GitHub release check can now be disabled five ways: the `--no-update-check` flag, the `DO_NOT_TRACK=1` (cross-tool standard) or `TTL_NO_UPDATE_CHECK=1` environment variables, a `no_update_check = true` preference in `~/.config/ttl/config.toml`, the **Update Check** toggle in the TUI Settings modal, or a build with `--no-default-features`, which compiles the check out entirely and drops the `update-informer` dependency. Addresses package-repository, air-gapped, and privacy use cases where an unconditional phone-home is unexpected. An [`examples/config.toml`](examples/config.toml) now documents the available preferences. + +### Security +- **`crossbeam-epoch` 0.9.18 → 0.9.20** clears RUSTSEC-2026-0204 (invalid pointer dereference in the `fmt::Pointer` impl for `Atomic`/`Shared`). The advisory reaches ttl transitively via `moka` → `hickory-resolver`; the bump keeps `cargo audit` clean. + ## [0.20.2] - 2026-07-06 ### Fixed diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/CONTRIBUTING.md new/ttl-0.21.0/CONTRIBUTING.md --- old/ttl-0.20.2/CONTRIBUTING.md 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/CONTRIBUTING.md 2026-07-07 16:42:22.000000000 +0200 @@ -69,6 +69,36 @@ After install, hooks run automatically. To run them manually against staged files: `prek run` (or `pre-commit run`). +## Minimum Supported Rust Version (MSRV) + +ttl targets a **modest MSRV** rather than tracking the latest stable release. The +current floor is **Rust 1.88** (edition 2024), declared as `rust-version` in +`Cargo.toml` and enforced by the `msrv` CI job, which pins +`dtolnay/[email protected]` and runs `cargo check`. + +Policy: + +- **MSRV bumps are deliberate and manual — never automated.** Dependabot's + `github-actions` group is configured to `ignore` `dtolnay/rust-toolchain`, + because that action is tagged by *Rust version*: without the ignore, Dependabot + reads the `@1.88` MSRV pin as a stale tag and tries to bump it to the newest + Rust, silently defeating the check. +- **Dependency updates that raise the required Rust are caught, not merged + blindly.** If a `cargo` update needs newer than the MSRV, the `msrv` job goes + red. That's the decision point: pin the older dependency, or bump the MSRV on + purpose. Edition 2024 also uses the MSRV-aware resolver, so `cargo update` + prefers versions compatible with `rust-version` when one exists. +- **When to re-evaluate:** when the `msrv` job forces it, when you genuinely need + a language/std feature stabilized after the current floor, or on a loose cadence + (a quick glance each minor release). A reasonable "modest" target is the Rust + shipped by current Debian stable / Ubuntu LTS, or roughly stable minus ~4 + releases — don't chase the newest. + +To bump the MSRV when warranted, update **both** `rust-version` in `Cargo.toml` +and the pinned version in the `msrv` job in `.github/workflows/ci.yml` (and the +`Rust 1.88+` line under Prerequisites above). The `msrv` job then verifies the +new floor actually builds. + ## Testing ```bash diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/Cargo.lock new/ttl-0.21.0/Cargo.lock --- old/ttl-0.20.2/Cargo.lock 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/Cargo.lock 2026-07-07 16:42:22.000000000 +0200 @@ -519,9 +519,9 @@ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -3601,7 +3601,7 @@ [[package]] name = "ttl" -version = "0.20.2" +version = "0.21.0" dependencies = [ "anyhow", "chrono", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/Cargo.toml new/ttl-0.21.0/Cargo.toml --- old/ttl-0.20.2/Cargo.toml 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/Cargo.toml 2026-07-07 16:42:22.000000000 +0200 @@ -1,6 +1,6 @@ [package] name = "ttl" -version = "0.20.2" +version = "0.21.0" edition = "2024" rust-version = "1.88" description = "Modern traceroute/mtr-style TUI with hop stats and optional ASN/geo enrichment" @@ -9,6 +9,13 @@ keywords = ["traceroute", "mtr", "network", "tui", "diagnostics"] categories = ["command-line-utilities", "network-programming"] +[features] +default = ["update-check"] +# Bundles the startup version check (a network call to the GitHub releases API). +# Packagers can drop it entirely with `--no-default-features`, which also drops +# the update-informer dependency. +update-check = ["dep:update-informer"] + [dependencies] # Async runtime tokio = { version = "1", features = ["full"] } @@ -58,7 +65,7 @@ dirs = "6.0" # Update notifications -update-informer = { version = "1", default-features = false, features = ["github", "ureq", "rustls-tls"] } +update-informer = { version = "1", default-features = false, features = ["github", "ureq", "rustls-tls"], optional = true } # TTY detection is-terminal = "0.4" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/README.md new/ttl-0.21.0/README.md --- old/ttl-0.20.2/README.md 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/README.md 2026-07-07 16:42:22.000000000 +0200 @@ -6,7 +6,9 @@ Network diagnostic tool that goes beyond traceroute: MTU discovery, NAT detection, route flap alerts, IX identification, and more. - +<p align="center"> + <img src="docs/demo.gif" alt="ttl live traceroute TUI: hops with ASN and reverse-DNS enrichment, per-hop loss/latency/jitter stats, sparklines, and a hop detail view" width="900"> +</p> [](https://crates.io/crates/ttl) [](https://github.com/lance0/ttl/actions/workflows/ci.yml) @@ -43,7 +45,7 @@ - **MPLS label detection** from ICMP extensions - **ICMP, UDP, TCP probing** with auto-detection - **Great TUI** with themes, sparklines, and session export -- **Update notifications** - in-app banner when new versions are available +- **Update notifications** - in-app banner when new versions are available (opt out via `--no-update-check`, `DO_NOT_TRACK`, config, or a `--no-default-features` build) - **Scriptable** - JSON, CSV, text report, and line-delimited JSON streaming output - **Trace diffing** - compare two saved sessions for path and latency changes - **Daemon mode + Prometheus exporter** - headless continuous monitoring with `/metrics` and `/healthz` @@ -225,6 +227,7 @@ ttl --size 1400 host # Large packets for MTU testing ttl --dscp 46 host # QoS marking (EF) ttl --wide host # Wide mode for wider terminals +ttl --no-update-check host # Skip the startup release check (see also DO_NOT_TRACK) ``` See [docs/FEATURES.md](docs/FEATURES.md) for full CLI reference. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/ROADMAP.md new/ttl-0.21.0/ROADMAP.md --- old/ttl-0.20.2/ROADMAP.md 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/ROADMAP.md 2026-07-07 16:42:22.000000000 +0200 @@ -99,6 +99,12 @@ --- +## Completed (v0.21.0) + +- [x] Opt out of the startup update check (#110): `--no-update-check` flag, `DO_NOT_TRACK`/`TTL_NO_UPDATE_CHECK` env vars, `no_update_check` config key, a TUI Settings toggle, and a `--no-default-features` build that compiles the check (and `update-informer`) out entirely + +--- + ## Planned Features ### Before next release — TTL send-path correctness (follow-ups to #12) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/docs/FEATURES.md new/ttl-0.21.0/docs/FEATURES.md --- old/ttl-0.20.2/docs/FEATURES.md 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/docs/FEATURES.md 2026-07-07 16:42:22.000000000 +0200 @@ -419,6 +419,7 @@ - **Theme**: Select from 11 built-in themes with live preview - **Display Mode**: Control column widths (auto/compact/wide) - **PeeringDB**: Configure API key and view cache status (only shown when IX detection is enabled) +- **Update Check**: Toggle the startup check for new releases on/off (persisted to config) ### Navigation @@ -426,7 +427,7 @@ |-----|--------| | `Tab` | Switch between sections | | `Up`/`Down` or `j`/`k` | Navigate within section | -| `Enter` or `Space` | Cycle option (theme/display mode) | +| `Enter` or `Space` | Cycle option (theme/display mode) or toggle update check | | `r` | Refresh PeeringDB cache (in PeeringDB section) | | `Esc` | Close and save | @@ -483,6 +484,21 @@ - Pre-built binary: link to GitHub releases - **Non-interactive mode**: Update notice is printed to stderr after the run completes (JSON/CSV/report modes) +### Disabling the update check + +The check can be turned off — useful for packaged installs, air-gapped hosts, or +privacy. Any of these opts out (checked in this order): + +- **Per run**: `ttl --no-update-check <target>` +- **Environment**: `DO_NOT_TRACK=1` (the [cross-tool standard](https://consoledonottrack.com/)) + or `TTL_NO_UPDATE_CHECK=1`. Any value other than empty/`0`/`false` counts as set. +- **Persistent**: `no_update_check = true` in `~/.config/ttl/config.toml` (see + [`examples/config.toml`](../examples/config.toml)), or toggle **Update Check** + off in the Settings modal (`s`), which saves the preference. +- **Compile-time**: build with `--no-default-features` to drop the check — and the + `update-informer` dependency — entirely. Package maintainers can ship a build + that never phones home. + ## Output Formats ### JSON Binary files old/ttl-0.20.2/docs/demo.gif and new/ttl-0.21.0/docs/demo.gif differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/docs/demo.tape new/ttl-0.21.0/docs/demo.tape --- old/ttl-0.20.2/docs/demo.tape 1970-01-01 01:00:00.000000000 +0100 +++ new/ttl-0.21.0/docs/demo.tape 2026-07-07 16:42:22.000000000 +0200 @@ -0,0 +1,47 @@ +# ttl — README demo cassette (live TUI traceroute) +# +# Rendered with VHS (https://github.com/charmbracelet/vhs): +# PATH="$HOME/go/bin:$HOME/.local/bin:$PATH" vhs docs/demo.tape +# +# Captures the REAL TUI on a live trace. Raw-socket privilege comes from a +# throwaway Docker container (--cap-add=NET_RAW), so no host setcap/sudo is +# needed; the freshly built ./target/release/ttl is mounted in. Reproducible: +# `cargo build --release` then re-run the vhs command from the repo root. + +Output docs/demo.gif + +Set Width 1280 +Set Height 760 +Set FontSize 15 +Set Padding 16 +Set Theme "Catppuccin Mocha" +Set CursorBlink false + +# --- clean prompt ------------------------------------------------------------- +Hide +Type "clear" +Enter +Sleep 500ms +Show + +# --- launch the real TUI on a live trace (raw sockets via Docker NET_RAW) ------ +Type@35ms "docker run --rm -it --cap-add=NET_RAW -v $PWD:/w -w /w ubuntu:24.04 ./target/release/ttl 1.1.1.1" +Enter +Sleep 12s # container start + live trace: hops appear, ASNs + rDNS resolve, sparklines build + +# --- open a hop's detail view (percentiles, jitter, routing symmetry, TTL) ---- +Down +Sleep 350ms +Down +Sleep 350ms +Down +Sleep 500ms +Enter +Sleep 4500ms +Escape +Sleep 3s # hold on the fully populated table (final good frame) + +# --- quit off-camera so the last frame is the TUI, not the shell prompt ------- +Hide +Type "q" +Sleep 800ms diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/examples/config.toml new/ttl-0.21.0/examples/config.toml --- old/ttl-0.20.2/examples/config.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/ttl-0.21.0/examples/config.toml 2026-07-07 16:42:22.000000000 +0200 @@ -0,0 +1,21 @@ +# ttl configuration file +# Place at ~/.config/ttl/config.toml +# +# ttl writes this file itself when you change settings in the TUI (theme, +# display mode, PeeringDB key). You can also edit it by hand. Every key is +# optional; omitted keys use the built-in defaults. + +# Color theme: default, kawaii, cyber, dracula, monochrome, matrix, nord, +# gruvbox, catppuccin, tokyo_night, solarized +theme = "default" + +# Column width mode: auto, compact, or wide +display_mode = "auto" + +# PeeringDB API key for higher-rate-limit IX detection lookups +# peeringdb_api_key = "your-key-here" + +# Disable the background check for a newer ttl release. Also honored via the +# DO_NOT_TRACK or TTL_NO_UPDATE_CHECK environment variables, the +# --no-update-check flag, or the Update Check toggle in the TUI settings. +no_update_check = false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/cli.rs new/ttl-0.21.0/src/cli.rs --- old/ttl-0.20.2/src/cli.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/cli.rs 2026-07-07 16:42:22.000000000 +0200 @@ -124,6 +124,11 @@ #[arg(long = "no-tui")] pub no_tui: bool, + /// Skip the startup check for new releases (also honors DO_NOT_TRACK=1 or + /// TTL_NO_UPDATE_CHECK=1, or a saved `no_update_check = true` preference) + #[arg(long = "no-update-check")] + pub no_update_check: bool, + /// Output JSON (batch mode, requires -c) #[arg(long = "json")] pub json: bool, @@ -475,6 +480,7 @@ no_ix: false, geoip_db: None, no_tui: false, + no_update_check: false, json: false, csv: false, report: false, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/main.rs new/ttl-0.21.0/src/main.rs --- old/ttl-0.20.2/src/main.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/main.rs 2026-07-07 16:42:22.000000000 +0200 @@ -70,13 +70,26 @@ return run_diff_mode(&diff_files[0], &diff_files[1], args.json); } - // Spawn background update check after early exits - // Uses channel for non-blocking result retrieval at exit - let (update_tx, update_rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let result = update::check_for_update(); - let _ = update_tx.send(result); // Ignore if receiver dropped - }); + // Spawn background update check after early exits, unless opted out by any + // source. Precedence (higher wins): compiled-out > env (DO_NOT_TRACK / + // TTL_NO_UPDATE_CHECK) > --no-update-check flag > saved `no_update_check` + // preference. Uses a channel for non-blocking result retrieval at exit. + // ponytail: re-reads the tiny prefs TOML here (the TUI reloads it later); a + // full prefs-threading refactor isn't worth it for one small file read. + let update_disabled = update::check_disabled( + update::env_opt_out(), + args.no_update_check, + Prefs::load().no_update_check, + ); + let update_rx = if update_disabled { + None + } else { + let (update_tx, update_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = update_tx.send(update::check_for_update()); // Ignore if receiver dropped + }); + Some(update_rx) + }; // Check permissions early if let Err(e) = check_permissions() { @@ -358,7 +371,8 @@ // Check for update notification (only for non-interactive mode) // Use short timeout so we don't delay exit if check is slow - if is_terminal::is_terminal(std::io::stderr()) + if let Some(update_rx) = update_rx + && is_terminal::is_terminal(std::io::stderr()) && let Ok(Some(new_version)) = update_rx.recv_timeout(Duration::from_millis(100)) { update::print_update_notice(&new_version); @@ -783,7 +797,7 @@ cancel: CancellationToken, interface: Option<InterfaceInfo>, resolve_info: Option<ResolveInfo>, - update_rx: std::sync::mpsc::Receiver<Option<String>>, + update_rx: Option<std::sync::mpsc::Receiver<Option<String>>>, ) -> Result<()> { // Shared pending map for probe correlation (engine writes, receiver reads) let pending = new_pending_map(); @@ -940,7 +954,7 @@ prefs, resolve_info, ix_lookup.clone(), - Some(update_rx), + update_rx, None, // replay_state (live mode) Some(add_target_tx), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/prefs.rs new/ttl-0.21.0/src/prefs.rs --- old/ttl-0.20.2/src/prefs.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/prefs.rs 2026-07-07 16:42:22.000000000 +0200 @@ -49,6 +49,11 @@ /// PeeringDB API key for higher rate limits on IX detection #[serde(default, skip_serializing_if = "Option::is_none")] pub peeringdb_api_key: Option<String>, + /// Disable the background check for a newer ttl release. Absent/`None` means + /// enabled (the default); `true` opts out. Also overridable per-run via + /// --no-update-check or the DO_NOT_TRACK / TTL_NO_UPDATE_CHECK env vars. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub no_update_check: Option<bool>, } impl Prefs { @@ -137,6 +142,24 @@ assert!(prefs.theme.is_none()); assert!(prefs.display_mode.is_none()); assert!(prefs.peeringdb_api_key.is_none()); + assert!(prefs.no_update_check.is_none()); + } + + #[test] + fn test_prefs_no_update_check_roundtrip() { + // Some(true) is persisted; None is omitted (default = enabled). + let opted_out = Prefs { + no_update_check: Some(true), + ..Default::default() + }; + let toml_str = toml::to_string_pretty(&opted_out).unwrap(); + assert!(toml_str.contains("no_update_check = true")); + let loaded: Prefs = toml::from_str(&toml_str).unwrap(); + assert_eq!(loaded.no_update_check, Some(true)); + + let default = Prefs::default(); + let toml_str = toml::to_string_pretty(&default).unwrap(); + assert!(!toml_str.contains("no_update_check")); } #[test] @@ -145,6 +168,7 @@ theme: Some("dracula".to_string()), display_mode: Some(DisplayMode::Wide), peeringdb_api_key: Some("test_api_key_123".to_string()), + no_update_check: None, }; let toml_str = toml::to_string_pretty(&prefs).unwrap(); assert!(toml_str.contains("theme = \"dracula\"")); @@ -166,6 +190,7 @@ theme: Some("default".to_string()), display_mode: None, peeringdb_api_key: None, + no_update_check: None, }; let toml_str = toml::to_string_pretty(&prefs).unwrap(); // peeringdb_api_key should be omitted when None @@ -208,6 +233,7 @@ theme: None, display_mode: Some(DisplayMode::Auto), peeringdb_api_key: None, + no_update_check: None, }; let toml_str = toml::to_string_pretty(&prefs).unwrap(); assert!(toml_str.contains("display_mode = \"auto\"")); @@ -219,6 +245,7 @@ theme: None, display_mode: Some(DisplayMode::Compact), peeringdb_api_key: None, + no_update_check: None, }; let toml_str = toml::to_string_pretty(&prefs).unwrap(); assert!(toml_str.contains("display_mode = \"compact\"")); @@ -230,6 +257,7 @@ theme: None, display_mode: Some(DisplayMode::Wide), peeringdb_api_key: None, + no_update_check: None, }; let toml_str = toml::to_string_pretty(&prefs).unwrap(); assert!(toml_str.contains("display_mode = \"wide\"")); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/probe/icmp.rs new/ttl-0.21.0/src/probe/icmp.rs --- old/ttl-0.20.2/src/probe/icmp.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/probe/icmp.rs 2026-07-07 16:42:22.000000000 +0200 @@ -9,7 +9,7 @@ pub const DEFAULT_PAYLOAD_SIZE: usize = 56; /// Minimum payload size (4 bytes ProbeId + 4 bytes timestamp) pub const MIN_PAYLOAD_SIZE: usize = 8; -/// PMTUD marker byte written to payload[8] to distinguish PMTUD probes. +/// PMTUD marker byte written to `payload[8]` to distinguish PMTUD probes. /// Echo Replies echo the payload verbatim, so the receiver can detect /// PMTUD success without a separate ICMP identifier. pub const PMTUD_MARKER: u8 = 0x50; // 'P' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/tui/app.rs new/ttl-0.21.0/src/tui/app.rs --- old/ttl-0.20.2/src/tui/app.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/tui/app.rs 2026-07-07 16:42:22.000000000 +0200 @@ -165,10 +165,14 @@ let display_mode = initial_prefs.display_mode.unwrap_or_default(); let api_key = initial_prefs.peeringdb_api_key.clone(); + let mut settings = SettingsState::new(initial_index, display_mode, api_key); + // Toggle shows "enabled"; the pref stores the negated "disabled" form. + settings.update_check = !initial_prefs.no_update_check.unwrap_or(false); + let mut ui_state = UiState { theme_index: initial_index, display_mode, - settings: SettingsState::new(initial_index, display_mode, api_key), + settings, update_rx, replay_state, ..Default::default() @@ -216,6 +220,8 @@ theme: Some(theme_names[ui_state.theme_index].to_string()), display_mode: Some(ui_state.display_mode), peeringdb_api_key: final_api_key, + // Toggle holds "enabled"; persist the negated "disabled" form. + no_update_check: Some(!ui_state.settings.update_check), }) } @@ -747,9 +753,15 @@ ui_state.settings.next_section(ix_enabled); } KeyCode::Enter | KeyCode::Char(' ') => { - ui_state.settings.select(); - // Live preview display mode changes - ui_state.display_mode = ui_state.settings.display_mode; + if ui_state.settings.selected_section + == SettingsState::update_check_section(ix_enabled) + { + ui_state.settings.update_check = !ui_state.settings.update_check; + } else { + ui_state.settings.select(); + // Live preview display mode changes + ui_state.display_mode = ui_state.settings.display_mode; + } } _ => {} } @@ -952,17 +964,20 @@ ui_state.set_status(format!("Display: {}", ui_state.display_mode.label())); } KeyCode::Char('s') => { - // Open settings modal - preserve existing API key + // Open settings modal - preserve existing API key and the + // update-check toggle across opens. let current_api_key = if ui_state.settings.api_key.is_empty() { None } else { Some(ui_state.settings.api_key.clone()) }; + let update_check = ui_state.settings.update_check; ui_state.settings = SettingsState::new( ui_state.theme_index, ui_state.display_mode, current_api_key, ); + ui_state.settings.update_check = update_check; ui_state.show_settings = true; } // Open target input modal (live mode only) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/tui/views/settings.rs new/ttl-0.21.0/src/tui/views/settings.rs --- old/ttl-0.20.2/src/tui/views/settings.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/tui/views/settings.rs 2026-07-07 16:42:22.000000000 +0200 @@ -23,6 +23,8 @@ pub api_key: String, /// Cursor position in API key input pub api_key_cursor: usize, + /// Whether the startup update check is enabled (persisted to prefs) + pub update_check: bool, } impl SettingsState { @@ -36,9 +38,17 @@ display_mode, api_key, api_key_cursor: cursor, + // Caller overrides from prefs; enabled is the default. + update_check: true, } } + /// Section index of the Update Check toggle. It sits after the optional + /// PeeringDB section, so its index depends on whether IX detection is on. + pub fn update_check_section(ix_enabled: bool) -> usize { + if ix_enabled { 3 } else { 2 } + } + /// Insert a character at the cursor position pub fn handle_char(&mut self, c: char) { self.api_key.insert(self.api_key_cursor, c); @@ -123,9 +133,10 @@ // Display mode section has no up/down navigation } - /// Switch between sections (0=Theme, 1=Display Mode, 2=PeeringDB) + /// Switch between sections. Order: 0=Theme, 1=Display Mode, + /// 2=PeeringDB (only when IX detection is enabled), then Update Check last. pub fn next_section(&mut self, ix_enabled: bool) { - let num_sections = if ix_enabled { 3 } else { 2 }; + let num_sections = if ix_enabled { 4 } else { 3 }; self.selected_section = (self.selected_section + 1) % num_sections; } @@ -187,9 +198,10 @@ impl Widget for SettingsView<'_> { fn render(self, area: Rect, buf: &mut Buffer) { - // Calculate centered popup area (taller if IX is enabled) + // Calculate centered popup area (taller if IX is enabled). The extra 4 + // rows hold the Update Check section (blank + header + toggle + hint). let popup_width = 44.min(area.width.saturating_sub(4)); - let base_height = if self.ix_enabled { 23 } else { 17 }; + let base_height = if self.ix_enabled { 27 } else { 21 }; let popup_height = base_height.min(area.height.saturating_sub(4)); let popup_x = (area.width - popup_width) / 2 + area.x; let popup_y = (area.height - popup_height) / 2 + area.y; @@ -391,6 +403,39 @@ } } + // Update Check section (always shown; sits after the optional PeeringDB + // section so its index shifts with ix_enabled). + lines.push(Line::from("")); + let uc_section = SettingsState::update_check_section(self.ix_enabled); + let uc_selected = self.state.selected_section == uc_section; + let uc_header_style = if uc_selected { + Style::default() + .fg(self.theme.header) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(self.theme.text_dim) + }; + lines.push(Line::from(vec![Span::styled( + " Update Check", + uc_header_style, + )])); + let uc_state = if self.state.update_check { "on" } else { "off" }; + let uc_style = if uc_selected { + Style::default() + .fg(self.theme.shortcut) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(self.theme.text_dim) + }; + lines.push(Line::from(vec![Span::styled( + format!(" [{}] (Enter to toggle)", uc_state), + uc_style, + )])); + lines.push(Line::from(vec![Span::styled( + " check GitHub for new releases at startup", + Style::default().fg(self.theme.text_dim), + )])); + lines.push(Line::from("")); // Footer with keybindings @@ -457,21 +502,29 @@ fn test_settings_state_next_section() { let mut state = SettingsState::new(0, DisplayMode::Auto, None); - // Without IX enabled (2 sections) + // Without IX enabled: 3 sections (theme, display, update-check) assert_eq!(state.selected_section, 0); state.next_section(false); assert_eq!(state.selected_section, 1); state.next_section(false); + assert_eq!(state.selected_section, 2); // update-check + state.next_section(false); assert_eq!(state.selected_section, 0); // Wraps - // With IX enabled (3 sections) + // With IX enabled: 4 sections (theme, display, peeringdb, update-check) state.selected_section = 0; state.next_section(true); assert_eq!(state.selected_section, 1); state.next_section(true); assert_eq!(state.selected_section, 2); state.next_section(true); + assert_eq!(state.selected_section, 3); // update-check + state.next_section(true); assert_eq!(state.selected_section, 0); // Wraps + + // Update-check section index tracks ix_enabled. + assert_eq!(SettingsState::update_check_section(false), 2); + assert_eq!(SettingsState::update_check_section(true), 3); } #[test] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ttl-0.20.2/src/update.rs new/ttl-0.21.0/src/update.rs --- old/ttl-0.20.2/src/update.rs 2026-07-06 19:35:37.000000000 +0200 +++ new/ttl-0.21.0/src/update.rs 2026-07-07 16:42:22.000000000 +0200 @@ -2,8 +2,42 @@ //! //! Checks GitHub releases for new versions (cached, 1h interval). //! Detects install method and shows appropriate update command. +//! +//! The check can be opted out of via `--no-update-check`, the `DO_NOT_TRACK` +//! or `TTL_NO_UPDATE_CHECK` env vars, a `no_update_check = true` preference, or +//! by building without the default `update-check` feature. + +/// Whether update checking was compiled in (the `update-check` feature, on by +/// default). Package builds can drop it with `--no-default-features`, which also +/// removes the `update-informer` dependency from the tree. +pub const ENABLED: bool = cfg!(feature = "update-check"); + +/// True when the user opted out of the update check via environment: +/// `DO_NOT_TRACK` (the cross-tool standard, <https://consoledonottrack.com>) or +/// `TTL_NO_UPDATE_CHECK`. +pub fn env_opt_out() -> bool { + ["DO_NOT_TRACK", "TTL_NO_UPDATE_CHECK"] + .iter() + .any(|var| std::env::var(var).is_ok_and(|v| flag_is_truthy(&v))) +} -use update_informer::registry::GitHub; +/// A set env var counts as opt-out unless it's explicitly falsey (empty, "0", +/// or "false"). We err toward *not* phoning home when the value is ambiguous. +fn flag_is_truthy(v: &str) -> bool { + !matches!(v.trim(), "" | "0" | "false") +} + +/// Resolve whether the background update check should be skipped, honoring +/// precedence (higher layer wins): compiled-out > env opt-out > CLI flag > +/// saved `no_update_check` preference. +/// +/// `pref` is tristate: `Some(true)` opts out, `Some(false)` explicitly keeps +/// the check on, and `None` (unset) falls back to the compiled-in default +/// (enabled). ttl stores its config and TUI-saved prefs in one `config.toml`, +/// so there is a single preference layer here (xfr splits config vs. prefs). +pub fn check_disabled(env_opt_out: bool, cli_flag: bool, pref: Option<bool>) -> bool { + !ENABLED || env_opt_out || cli_flag || pref.unwrap_or(false) +} /// How ttl was installed (best guess based on binary path) #[derive(Debug, Clone, Copy)] @@ -59,9 +93,10 @@ /// Uses interval(ZERO) to always perform a network check — we only call this /// once per process lifetime (in a background thread), so cache-based rate /// limiting is unnecessary. +#[cfg(feature = "update-check")] pub fn check_for_update() -> Option<String> { use std::time::Duration; - use update_informer::Check; + use update_informer::{Check, registry::GitHub}; let informer = update_informer::new(GitHub, "lance0/ttl", env!("CARGO_PKG_VERSION")) .interval(Duration::ZERO); @@ -73,6 +108,12 @@ .map(|v| v.to_string()) } +/// No-op stub when built without the `update-check` feature. +#[cfg(not(feature = "update-check"))] +pub fn check_for_update() -> Option<String> { + None +} + /// Print update notification to stderr pub fn print_update_notice(new_version: &str) { let method = InstallMethod::detect(); @@ -105,6 +146,32 @@ use super::*; #[test] + fn test_flag_is_truthy() { + for on in ["1", "true", "yes", "on", " 1 "] { + assert!(flag_is_truthy(on), "{on:?} should count as opt-out"); + } + for off in ["", " ", "0", "false"] { + assert!(!flag_is_truthy(off), "{off:?} should not count as opt-out"); + } + } + + #[test] + fn test_check_disabled_precedence() { + // The tristate resolution only matters when the check is compiled in. + if !ENABLED { + assert!(check_disabled(false, false, Some(false))); + return; + } + // Saved pref governs when env/CLI are quiet. + assert!(check_disabled(false, false, Some(true))); // opt out + assert!(!check_disabled(false, false, Some(false))); // explicitly on + assert!(!check_disabled(false, false, None)); // default on + // env or CLI opt-out wins over an explicit pref "on". + assert!(check_disabled(true, false, Some(false))); + assert!(check_disabled(false, true, Some(false))); + } + + #[test] fn test_install_method_commands() { assert_eq!(InstallMethod::Homebrew.update_command(), "brew upgrade ttl"); assert_eq!(InstallMethod::Cargo.update_command(), "cargo install ttl"); Binary files old/ttl-0.20.2/ttlss.png and new/ttl-0.21.0/ttlss.png differ ++++++ ttl.obsinfo ++++++ --- /var/tmp/diff_new_pack.D8l3cM/_old 2026-07-09 22:19:32.130443926 +0200 +++ /var/tmp/diff_new_pack.D8l3cM/_new 2026-07-09 22:19:32.138444198 +0200 @@ -1,5 +1,5 @@ name: ttl -version: 0.20.2 -mtime: 1783359337 -commit: 11349e34e91844b2f6144ae4e6d412b3f9ae5220 +version: 0.21.0 +mtime: 1783435342 +commit: 46f1dc2cd8c055408a764d5674afa897ca7cd30c ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/ttl/vendor.tar.zst /work/SRC/openSUSE:Factory/.ttl.new.1991/vendor.tar.zst differ: char 7, line 1
