From 65c0ad2c77d04035ebd19f0e3b7eaec1cb291540 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 3 Oct 2026 12:30:15 +0300 Subject: [PATCH 01/37] feat: add a one-command windows build script --- .gitignore | 2 ++ README.md | 15 ++++++++++++ windows/setup.sh | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100755 windows/setup.sh diff --git a/.gitignore b/.gitignore index 88d9f7a..9da6713 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ result-* *.db *.db-wal *.db-shm +/dist +carrier-windows-*.zip diff --git a/README.md b/README.md index afa3be6..17285e0 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,18 @@ The result is installed into the user flatpak and runs as `flatpak run com.randogoth.carrier`. Inside the sandbox the app keeps its config under `~/.var/app/com.randogoth.carrier/config/carrier`, so a flatpak install starts with a fresh identity rather than the dev build's account. + +### Windows + +Install MSYS2 (`winget install MSYS2.MSYS2`), open the **MSYS2 MINGW64** +shell, and run the one-command script from the carrier checkout: + + git clone ssh://git@code.randogoth.com:2222/randogoth/carrier.git + cd carrier && windows/setup.sh + +`windows/setup.sh` installs the GTK and Rust stack into MSYS2, clones fumi +next to carrier if it is missing, builds, and bundles carrier.exe with the +GTK runtime into `dist/` and a zip — the complete Windows distribution. The +app keeps its config in `%APPDATA%\carrier`. If MSYS2 updates its core +mid-run the shell may close; rerun the script and it continues. Cloning +over SSH needs the key in `~/.ssh` inside the MSYS2 home. diff --git a/windows/setup.sh b/windows/setup.sh new file mode 100755 index 0000000..fa8e780 --- /dev/null +++ b/windows/setup.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Windows build in one command. Run inside the MSYS2 MINGW64 shell from +# the carrier checkout: it installs the dependencies into MSYS2, clones +# fumi next to carrier if missing, builds, and bundles carrier plus the +# GTK runtime into dist/ and carrier-windows.zip. +# +# If MSYS2 updates its own core mid-run the shell may die — just rerun. +set -euo pipefail + +say() { printf '\n==> %s\n' "$*"; } +die() { printf '\n!! %s\n' "$*" >&2; exit 1; } + +[ -d ../fumi/core ] || { + say "fumi not found next to carrier — cloning it" + git clone ssh://git@code.randogoth.com:2222/randogoth/fumi.git ../fumi \ + || die "cloning fumi failed — check your SSH key in ~/.ssh" +} + +say "installing dependencies (first run downloads several hundred MB)" +pacman -Syu --noconfirm || die "pacman update failed — reopen the shell and rerun this script" +pacman -S --noconfirm --needed \ + mingw-w64-x86_64-gtk4 \ + mingw-w64-x86_64-libadwaita \ + mingw-w64-x86_64-adwaita-icon-theme \ + mingw-w64-x86_64-toolchain \ + mingw-w64-x86_64-rust \ + mingw-w64-x86_64-git \ + zip \ + || die "installing the build stack failed" + +pkg-config --exists gtk4 libadwaita-1 \ + || die "gtk4/libadwaita invisible to pkg-config — are you in the MINGW64 shell?" + +say "building carrier (release; the first build takes a while)" +cargo build --release --locked || die "cargo build failed" + +say "bundling the runtime" +rm -rf dist +mkdir -p dist/share/glib-2.0/schemas \ + dist/share/icons/hicolor/512x512/apps \ + dist/share/icons/hicolor/256x256/apps \ + dist/share/icons/hicolor/128x128/apps +cp target/release/carrier.exe dist/ +for dll in $(ldd target/release/carrier.exe | awk '/mingw64/ {print $3}'); do + cp "$dll" dist/ || die "copying $dll failed" +done +[ "$(ls dist/*.dll | wc -l)" -gt 0 ] || die "no DLLs collected — ldd found none under /mingw64" +cp /mingw64/share/glib-2.0/schemas/* dist/share/glib-2.0/schemas/ +glib-compile-schemas dist/share/glib-2.0/schemas/ +cp -r /mingw64/share/icons/Adwaita dist/share/icons/ +cp -r /mingw64/share/icons/hicolor dist/share/icons/ +cp data/icons/com.randogoth.carrier.png dist/share/icons/hicolor/512x512/apps/ +cp data/icons/com.randogoth.carrier-256.png dist/share/icons/hicolor/256x256/apps/ +cp data/icons/com.randogoth.carrier-128.png dist/share/icons/hicolor/128x128/apps/ + +say "zipping" +version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) +rm -f "carrier-windows-$version.zip" +(cd dist && zip -qr "../carrier-windows-$version.zip" .) + +say "done — test with ./dist/carrier.exe (SmartScreen may ask once)" From 0b20b56cbaf5b607066faea2627fc828baa82ade Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 3 Oct 2026 13:58:10 +0300 Subject: [PATCH 02/37] feat: embed the carrier icon in the Windows exe --- Cargo.lock | 11 +++++++++++ Cargo.toml | 3 +++ build.rs | 10 ++++++++++ data/icons/com.randogoth.carrier.ico | Bin 0 -> 42970 bytes 4 files changed, 24 insertions(+) create mode 100644 build.rs create mode 100644 data/icons/com.randogoth.carrier.ico diff --git a/Cargo.lock b/Cargo.lock index d4eb85f..6644ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "rand_core", "relm4", "tempfile", + "winresource", ] [[package]] @@ -1630,6 +1631,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/Cargo.toml b/Cargo.toml index 6773e0a..c81e242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,5 +15,8 @@ relm4 = { version = "0.9", features = ["libadwaita"] } rand_core = { version = "0.6", features = ["getrandom"] } qrcodegen = "1" +[build-dependencies] +winresource = "0.1" + [dev-dependencies] tempfile = "3" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b242a43 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +// Windows reads the taskbar and Explorer icon from the exe's resource +// section; desktop environments on other platforms use the desktop file. +fn main() { + println!("cargo:rerun-if-changed=data/icons/com.randogoth.carrier.ico"); + if std::env::var("CARGO_CFG_WINDOWS").is_ok() { + let mut res = winresource::WindowsResource::new(); + res.set_icon("data/icons/com.randogoth.carrier.ico"); + res.compile().expect("failed to embed Windows icon"); + } +} diff --git a/data/icons/com.randogoth.carrier.ico b/data/icons/com.randogoth.carrier.ico new file mode 100644 index 0000000000000000000000000000000000000000..8305c58f7014512ffbb58c9f6ea32f686c1fbaef GIT binary patch literal 42970 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 F9RSmQ+qeJ# literal 0 HcmV?d00001 From 74ec9a24921b0f1d46a2c38f9499628721abe435 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 12:48:09 +0300 Subject: [PATCH 03/37] feat: simplify devbox config for macOS aarch64 On Apple Silicon Macs, many GTK-related nixpkgs packages are marked as unsupported on aarch64-darwin. Instead of using nixpkgs for the GUI stack, use Homebrew which provides pre-built bottles for gtk4, libadwaita, and all their dependencies (glib, pango, cairo, gdk-pixbuf, graphene, etc.). The devbox config now provides only the Rust toolchain (rustc, cargo, pkg-config, rustfmt) and sets environment variables to locate the Homebrew-installed libraries: - PKG_CONFIG_PATH: /opt/homebrew/lib/pkgconfig - PKG_CONFIG_PATH_arm64_apple_darwin: /opt/homebrew/lib/pkgconfig - PATH: Includes /opt/homebrew/bin and /opt/homebrew/sbin This allows building the project with: devbox run build All 16 tests pass with: devbox run check Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- devbox.json | 29 ++++----------- devbox.lock | 100 ++++++++-------------------------------------------- 2 files changed, 21 insertions(+), 108 deletions(-) diff --git a/devbox.json b/devbox.json index a517954..be95b18 100644 --- a/devbox.json +++ b/devbox.json @@ -4,30 +4,15 @@ "rustc@latest", "cargo@latest", "pkg-config@latest", - "rustfmt@latest", - "nixpkgs#wayland", - "nixpkgs#libxkbcommon", - "nixpkgs#mesa", - "nixpkgs#libGL", - "nixpkgs#vulkan-loader", - "nixpkgs#gcc", - "nixpkgs#gtk4", - "nixpkgs#glib", - "nixpkgs#pango", - "nixpkgs#cairo", - "nixpkgs#graphene", - "nixpkgs#gdk-pixbuf", - "nixpkgs#harfbuzz", - "nixpkgs#libadwaita", - "nixpkgs#adwaita-icon-theme", - "nixpkgs#flatpak", - "nixpkgs#flatpak-builder", - "nixpkgs#appstream" + "rustfmt@latest" ], + "env": { + "PATH": "/opt/homebrew/bin:/opt/homebrew/sbin:$PATH", + "PKG_CONFIG_PATH": "/opt/homebrew/lib/pkgconfig", + "PKG_CONFIG_PATH_arm64_apple_darwin": "/opt/homebrew/lib/pkgconfig" + }, "shell": { - "init_hook": [ - ". \"$PWD/scripts/devbox-libs.sh\"" - ], + "init_hook": [], "scripts": { "build": [ "cargo build --release" diff --git a/devbox.lock b/devbox.lock index a6f3109..d9c9eac 100644 --- a/devbox.lock +++ b/devbox.lock @@ -43,81 +43,9 @@ "last_modified": "2026-09-29T03:29:29Z", "resolved": "github:NixOS/nixpkgs/b6c8664de9b6cc07fe5666a29f91884ba81197c4?lastModified=1790652569&narHash=sha256-641r7xrlSOHYoktL9%2FaArPIYNgZJ9eM5URzSEOtPJDY%3D" }, - "nixpkgs#adwaita-icon-theme": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#adwaita-icon-theme" - }, - "nixpkgs#appstream": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#appstream" - }, - "nixpkgs#cairo": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#cairo" - }, - "nixpkgs#flatpak": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#flatpak" - }, - "nixpkgs#flatpak-builder": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#flatpak-builder" - }, - "nixpkgs#gcc": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#gcc" - }, - "nixpkgs#gdk-pixbuf": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#gdk-pixbuf" - }, - "nixpkgs#glib": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#glib" - }, - "nixpkgs#graphene": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#graphene" - }, - "nixpkgs#gtk4": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#gtk4" - }, - "nixpkgs#harfbuzz": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#harfbuzz" - }, - "nixpkgs#libGL": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#libGL" - }, - "nixpkgs#libadwaita": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#libadwaita" - }, - "nixpkgs#libxkbcommon": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#libxkbcommon" - }, - "nixpkgs#mesa": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#mesa" - }, - "nixpkgs#pango": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#pango" - }, - "nixpkgs#vulkan-loader": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#vulkan-loader" - }, - "nixpkgs#wayland": { - "last_modified": "1970-01-01T00:00:00Z", - "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#wayland" - }, "pkg-config@latest": { - "last_modified": "2025-11-23T21:50:36Z", - "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#pkg-config", + "last_modified": "2026-10-05T09:07:57Z", + "resolved": "github:NixOS/nixpkgs/aa48d347080940b8a2b8d2f48228674e280a3514#pkg-config", "source": "devbox-search", "version": "0.29.2", "systems": { @@ -125,39 +53,39 @@ "outputs": [ { "name": "out", - "path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2", + "path": "/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2", "default": true }, { "name": "man", - "path": "/nix/store/9px0sji43x3r2w4zxl3j3idwsql7lwxx-pkg-config-wrapper-0.29.2-man", + "path": "/nix/store/rk4dw0mj2lakbw5hml11mfpmdx9xjzfa-pkg-config-wrapper-0.29.2-man", "default": true }, { "name": "doc", - "path": "/nix/store/hqk44ra6qxw7iixardl6c3hdgb9kq6ns-pkg-config-wrapper-0.29.2-doc" + "path": "/nix/store/xzlcs9f8xl9dvnbwifmlfbxy15jq53j1-pkg-config-wrapper-0.29.2-doc" } ], - "store_path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2" + "store_path": "/nix/store/nhgylvj98klqggy54h7gbc3b2ia4a00y-pkg-config-wrapper-0.29.2" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2", + "path": "/nix/store/h82n3d0wdrgskdc3yvmz5l3c50h2snvp-pkg-config-wrapper-0.29.2", "default": true }, { "name": "man", - "path": "/nix/store/10060k24qggqyzlwdsfmni9y32zxcg0j-pkg-config-wrapper-0.29.2-man", + "path": "/nix/store/8960pp9vb5hx49q3h315hwr33gb0k2k0-pkg-config-wrapper-0.29.2-man", "default": true }, { "name": "doc", - "path": "/nix/store/0y4v51ndpyvkj09hwlfqkz0c3h17zfmc-pkg-config-wrapper-0.29.2-doc" + "path": "/nix/store/dxpxspf67kl1gydpl3v3f7pxa3l9wyvq-pkg-config-wrapper-0.29.2-doc" } ], - "store_path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2" + "store_path": "/nix/store/h82n3d0wdrgskdc3yvmz5l3c50h2snvp-pkg-config-wrapper-0.29.2" }, "x86_64-darwin": { "outputs": [ @@ -182,20 +110,20 @@ "outputs": [ { "name": "out", - "path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2", + "path": "/nix/store/b9izmrfjp0qnnyahdwwrzdwi87s1h179-pkg-config-wrapper-0.29.2", "default": true }, { "name": "man", - "path": "/nix/store/j9xfpnrygg3v37svc5pfin9q5bm49r94-pkg-config-wrapper-0.29.2-man", + "path": "/nix/store/c8asqjndyv84qh5y2z84w9np0c369ill-pkg-config-wrapper-0.29.2-man", "default": true }, { "name": "doc", - "path": "/nix/store/x3bypxdxaq20kykybhkf21x4jczsiy8y-pkg-config-wrapper-0.29.2-doc" + "path": "/nix/store/8fsw8ybdnhcif89dmk8pp85ykjpvh3pr-pkg-config-wrapper-0.29.2-doc" } ], - "store_path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2" + "store_path": "/nix/store/b9izmrfjp0qnnyahdwwrzdwi87s1h179-pkg-config-wrapper-0.29.2" } } }, From 68f5620d35713a3d4a419f7a44f7fe57c8e806a4 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 3 Oct 2026 16:45:07 +0300 Subject: [PATCH 04/37] feat: let composes carry custom frontmatter headers --- README.md | 1 + src/app.rs | 122 ++++++++++++++++++++++++++++++++- src/demo.rs | 1 + src/ops.rs | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/view.rs | 98 ++++++++++++++++++++++++++- 5 files changed, 401 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 17285e0..7682a83 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A minimalist desktop client for [Smol Mail](../smolmail): one compact window — - Pin a server's key — paste the operator's, or connect and pin what it presents — then register a username or recall an existing address. - Fetch (with cancel), read, reply and send sealed mail; the inbox, the requests tier and sent copies each keep their own folder. - Composes closed by any sidebar trip are stashed as drafts — kept beside the store, reopened from a folder that appears only once one exists; Cancel is the only way to throw one away. +- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. - Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. - Contacts with verified/tofu marks and smol:// import; your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. - Key rotation with a confirm-first step, server pin management, a network timeout, and gsmol backup export/import that never contains the seed. diff --git a/src/app.rs b/src/app.rs index 8859f80..b58c462 100644 --- a/src/app.rs +++ b/src/app.rs @@ -93,6 +93,8 @@ pub struct Compose { pub body: String, /// The message this replies to, as 64 hex characters. pub reply_to: Option, + /// Frontmatter rows as typed — `Key: value` text, parsed at send. + pub headers: Vec, } #[derive(Debug, Clone)] @@ -146,6 +148,9 @@ pub enum Message { ComposeToChanged(String), ComposeSubjectChanged(String), ComposeBodyChanged(String), + ComposeHeaderAdded, + ComposeHeaderChanged(usize, String), + ComposeHeaderRemoved(usize), ComposeClosed, SendPressed, ReplyPressed, @@ -225,6 +230,7 @@ pub enum PendingOp { subject: String, body: String, reply_to: Option, + headers: Vec<(String, String)>, }, Delete { id: String, @@ -372,9 +378,11 @@ impl Carrier { let Some(compose) = self.compose.take() else { return; }; + let rows = compose.headers.iter().any(|row| !row.trim().is_empty()); if compose.to.trim().is_empty() && compose.subject.trim().is_empty() && compose.body.trim().is_empty() + && !rows { return; } @@ -384,6 +392,7 @@ impl Carrier { subject: compose.subject, body: compose.body, reply_to: compose.reply_to, + headers: compose.headers, at: fumi_core::store::now(), }); ops::save_drafts(&self.drafts_path(), &self.drafts); @@ -403,6 +412,7 @@ impl Carrier { subject: draft.subject, body: draft.body, reply_to: draft.reply_to, + headers: draft.headers, }); self.compose_fresh.set(true); } @@ -518,6 +528,25 @@ impl Carrier { compose.body = body; } } + Message::ComposeHeaderAdded => { + if let Some(compose) = &mut self.compose { + compose.headers.push(String::new()); + } + } + Message::ComposeHeaderChanged(index, text) => { + if let Some(compose) = &mut self.compose { + if let Some(row) = compose.headers.get_mut(index) { + *row = text; + } + } + } + Message::ComposeHeaderRemoved(index) => { + if let Some(compose) = &mut self.compose { + if index < compose.headers.len() { + compose.headers.remove(index); + } + } + } Message::ImportBodyChanged(text) => { self.import_body = text; } @@ -712,6 +741,7 @@ impl Carrier { subject: String::new(), body: String::new(), reply_to: None, + headers: Vec::new(), }); self.compose_fresh.set(true); } @@ -738,6 +768,7 @@ impl Carrier { subject, body: String::new(), reply_to: Some(msg.id.clone()), + headers: Vec::new(), }); self.compose_fresh.set(true); } @@ -752,11 +783,27 @@ impl Carrier { self.fail("enter a recipient".into()); return Vec::new(); } + let mut headers = Vec::new(); + for (index, row) in compose.headers.iter().enumerate() { + match ops::header_row(row) { + Ok(Some(pair)) => headers.push(pair), + Ok(None) => {} + Err(reason) => { + self.fail(format!("header {}: {reason}", index + 1)); + return Vec::new(); + } + } + } + if let Err(reason) = ops::validate_headers(&headers) { + self.fail(reason); + return Vec::new(); + } let op = PendingOp::Send { to: compose.to.trim().to_string(), subject: compose.subject.trim().to_string(), body: compose.body.clone(), reply_to: compose.reply_to.clone(), + headers, }; self.compose = None; return vec![op]; @@ -850,6 +897,7 @@ impl Carrier { subject: String::new(), body: String::new(), reply_to: None, + headers: Vec::new(), }); self.compose_fresh.set(true); } @@ -1042,7 +1090,6 @@ mod tests { } } - #[test] #[test] fn contact_selection_and_message_selection_supersede_each_other() { let mut app = app(); @@ -1090,6 +1137,7 @@ mod tests { subject: String::new(), body: String::new(), reply_to: None, + headers: Vec::new(), }); app.busy = Some("fetching".into()); app.handle(Message::ComposeBodyChanged("typing while busy".into())); @@ -1145,6 +1193,78 @@ mod tests { assert!(app.drafts.is_empty()); } + #[test] + fn header_rows_pass_the_busy_guard_and_edit_the_compose() { + let mut app = app(); + app.handle(Message::ComposePressed); + app.busy = Some("fetching".into()); + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderChanged(0, "Urgency: high".into())); + app.handle(Message::ComposeHeaderRemoved(1)); + assert_eq!( + app.compose.as_ref().expect("compose open").headers, + ["Urgency: high"] + ); + // An index that no longer exists is ignored, not a panic. + app.handle(Message::ComposeHeaderChanged(5, "no".into())); + assert_eq!(app.compose.as_ref().unwrap().headers, ["Urgency: high"]); + } + + #[test] + fn an_invalid_header_blocks_the_send() { + let mut app = app(); + app.handle(Message::ComposePressed); + app.handle(Message::ComposeToChanged("bob@localhost".into())); + + // A reserved key shadows the client's own field, so it is rejected. + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderChanged(0, "Subject: shadow".into())); + assert!(app.handle(Message::SendPressed).is_empty()); + assert!(app.compose.is_some()); + assert!(app.status_error); + + // Case-insensitive duplicates are rejected too. + app.handle(Message::ComposeHeaderChanged(0, "Urgency: high".into())); + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderChanged(1, "urgency: low".into())); + assert!(app.handle(Message::SendPressed).is_empty()); + assert!(app.status_error); + + // Removing the duplicate lets the send go through with the pair; + // the earlier error note stays until the op result replaces it. + app.handle(Message::ComposeHeaderRemoved(1)); + let ops = app.handle(Message::SendPressed); + assert_eq!(ops.len(), 1); + assert!(app.compose.is_none()); + assert!(matches!( + &ops[0], + PendingOp::Send { headers, .. } + if headers == &[("Urgency".to_string(), "high".to_string())] + )); + } + + #[test] + fn headers_survive_a_stash_and_reopen() { + let mut app = app(); + app.handle(Message::ComposePressed); + app.handle(Message::ComposeToChanged("bob@localhost".into())); + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderChanged(0, "Urgency: high".into())); + app.handle(Message::ComposeHeaderAdded); + app.handle(Message::ComposeHeaderChanged(1, "not valid yet".into())); + // A contact trip stashes without starting a refresh, so the + // reopen below is not swallowed by the busy guard. + app.handle(Message::ContactSelected("bob@localhost".into())); + assert_eq!(app.drafts.len(), 1); + let id = app.drafts[0].id.clone(); + app.handle(Message::MessageSelected(Folder::Drafts, id)); + let compose = app.compose.as_ref().expect("draft reopened"); + // Rows return exactly as typed, valid or not: validation is a + // send-time concern, not a stash-time one. + assert_eq!(compose.headers, ["Urgency: high", "not valid yet"]); + } + #[test] fn fetch_tick_lists_mail_as_it_lands() { use fumi_core::message::{build_frontmatter, message_id, seal}; diff --git a/src/demo.rs b/src/demo.rs index f93e7da..f74840e 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -227,6 +227,7 @@ fn plant(dir: &Path) -> Result<(), String> { to: "grace@example.org".into(), subject: "Re: quarterly infrastructure review".into(), body: "started a reply, will finish it after the review call".into(), + headers: vec!["Urgency: low".into()], reply_to: None, at: now() - 3 * 3600, }], diff --git a/src/ops.rs b/src/ops.rs index 2a3a577..eb9f417 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -11,6 +11,7 @@ use fumi_core::address::{Address, DEFAULT_PORT}; use fumi_core::client::{FetchOptions, TrustChange}; use fumi_core::crypto::{b32, fingerprint, unb32, KEY_LEN}; use fumi_core::error::Error; +use fumi_core::message::{FRONTMATTER_KEYS, FRONTMATTER_MAX}; use fumi_core::store::Store; use fumi_core::{account, client, export, store}; @@ -116,6 +117,8 @@ pub struct Draft { pub body: String, /// The message this draft replies to, as 64 hex characters. pub reply_to: Option, + /// Frontmatter rows as typed — `Key: value` text, parsed at send. + pub headers: Vec, pub at: i64, } @@ -133,6 +136,7 @@ fn line(text: &str) -> String { text.replace('\\', "\\\\") .replace('\n', "\\n") .replace('\r', "\\r") + .replace('\u{1f}', "\\\u{1e}") } fn unline(text: &str) -> String { @@ -143,6 +147,7 @@ fn unline(text: &str) -> String { match chars.next() { Some('n') => out.push('\n'), Some('r') => out.push('\r'), + Some('\u{1e}') => out.push('\u{1f}'), Some(other) => out.push(other), None => out.push('\\'), } @@ -159,11 +164,16 @@ pub fn save_drafts(path: &Path, drafts: &[Draft]) { let mut text = String::new(); for draft in drafts { let reply_to = draft.reply_to.as_deref().unwrap_or("-"); + let headers: Vec = draft.headers.iter().map(|row| line(row)).collect(); use std::fmt::Write; - let _ = writeln!(text, "draft {} {} {}", draft.id, draft.at, reply_to); + let _ = writeln!(text, "draft2 {} {} {}", draft.id, draft.at, reply_to); let _ = writeln!(text, "{}", line(&draft.to)); let _ = writeln!(text, "{}", line(&draft.subject)); let _ = writeln!(text, "{}", line(&draft.body)); + // Rows joined on a control separator, so any typed text is + // reversible; keys are [A-Za-z0-9-] but values can hold colons, + // semicolons — anything but a newline, which line() escapes. + let _ = writeln!(text, "{}", headers.join("\u{1f}")); } let _ = std::fs::write(path, text); } @@ -174,12 +184,12 @@ pub fn load_drafts(path: &Path) -> Vec { let text = std::fs::read_to_string(path).unwrap_or_default(); let lines: Vec<&str> = text.lines().collect(); let mut drafts = Vec::new(); - for record in lines.chunks(4) { - let [header, to, subject, body] = record else { - break; - }; + let mut index = 0; + while index + 4 <= lines.len() { + let header = lines[index]; + let (to, subject, body) = (lines[index + 1], lines[index + 2], lines[index + 3]); let mut parts = header.split_whitespace(); - let (Some("draft"), Some(id), Some(at), reply_to) = + let (kind, Some(id), Some(at), reply_to) = (parts.next(), parts.next(), parts.next(), parts.next()) else { break; @@ -188,6 +198,22 @@ pub fn load_drafts(path: &Path) -> Vec { break; }; let id = id.to_string(); + index += 4; + // draft2 carries a fifth line of header rows; the old draft + // records stop at four and the next line is another record. + let headers = if kind == Some("draft2") && index < lines.len() { + index += 1; + let raw = lines[index - 1]; + if raw.is_empty() { + Vec::new() + } else { + raw.split('\u{1f}').map(unline).collect() + } + } else if kind == Some("draft") { + Vec::new() + } else { + break; + }; drafts.push(Draft { id, to: unline(to), @@ -197,6 +223,7 @@ pub fn load_drafts(path: &Path) -> Vec { None | Some("-") => None, Some(other) => Some(other.to_string()), }, + headers, at, }); } @@ -369,6 +396,69 @@ pub fn fetch( Ok(note) } +/// Fields fumi itself attaches to every message (sec 5.7, 5.8); a +/// user copy of any of them would shadow the client's own, since the +/// reader keeps the first occurrence of a key. +const RESERVED_HEADERS: [&str; 4] = ["subject", "in-reply-to", "reply-to", "accept"]; + +/// Parses one compose header row as the user typed it — `Key: value`. +/// An empty row is allowed and simply skipped on send; anything else +/// that breaks the frontmatter block (sec 5.5) is rejected, because a +/// single malformed line makes the reader drop every field. +pub fn header_row(text: &str) -> Result, String> { + let text = text.trim(); + if text.is_empty() { + return Ok(None); + } + let Some((key, value)) = text.split_once(':') else { + return Err("a header is `Key: value` — the colon is missing".into()); + }; + let key = key.trim(); + if RESERVED_HEADERS + .iter() + .any(|reserved| key.eq_ignore_ascii_case(reserved)) + { + return Err(format!("{key} is attached by the client and cannot be set")); + } + if key.is_empty() + || key.len() > 64 + || !key + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + { + return Err("keys are 1-64 characters of letters, digits and dashes".into()); + } + if value.contains('\n') || value.contains('\r') { + return Err("a value must stay on one line".into()); + } + Ok(Some((key.to_string(), value.trim().to_string()))) +} + +/// The rules across all rows: keys are case-insensitive and must be +/// unique, and the whole block — fumi's own fields included — has to +/// stay inside the reader's budget (sec 5.5). +pub fn validate_headers(headers: &[(String, String)]) -> Result<(), String> { + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + for (key, _) in headers { + if !seen.insert(key.to_ascii_lowercase()) { + return Err(format!("{key} is set twice")); + } + } + let budget: usize = headers + .iter() + .map(|(key, value)| format!("{key}: {value}").len() + 1) + .sum(); + // The client adds Subject, In-Reply-To, a Reply-To URI and possibly an + // Accept token on top of the user's rows; 512 bytes covers them all. + if budget + 512 > FRONTMATTER_MAX { + return Err("the headers exceed the frontmatter budget".into()); + } + if headers.len() + 4 > FRONTMATTER_KEYS { + return Err("too many headers".into()); + } + Ok(()) +} + pub fn send( store: &Store, master: &[u8; KEY_LEN], @@ -376,16 +466,21 @@ pub fn send( subject: Option<&str>, body: &str, reply_to: Option<&str>, + headers: &[(String, String)], timeout: u64, ) -> Result { let addr = parse_address(to)?; let me = account(store, master)?; + let owned: Vec<(&str, &str)> = headers + .iter() + .map(|(key, value)| (key.as_str(), value.as_str())) + .collect(); let draft = client::SendDraft { address: &addr, text: body.to_string(), subject, reply_to, - headers: &[], + headers: &owned, anonymous: false, no_pad: false, }; @@ -652,6 +747,13 @@ mod tests { subject: "line breaks\nand \\backslashes\r".into(), body: "a draft\n\nwith paragraphs\nand a \\ literal".into(), reply_to: Some("ab".repeat(32)), + // Rows return as typed: colons and separators included. + headers: vec![ + "Urgency: high".into(), + "Note: a: value: with: colons".into(), + String::from_utf8_lossy(&[0x1f, b's', b'e', b'p']).into_owned(), + String::new(), + ], at: now(), }, Draft { @@ -660,6 +762,7 @@ mod tests { subject: "no reply".into(), body: String::new(), reply_to: None, + headers: Vec::new(), at: now(), }, ]; @@ -671,6 +774,78 @@ mod tests { let torn = text.lines().take(6).collect::>().join("\n") + "\n"; std::fs::write(&path, torn).unwrap(); assert_eq!(load_drafts(&path).len(), 1); + + // The old four-line records, written before header rows existed, + // still read as drafts with no headers. + let legacy = format!( + "draft {} {} {}\n{}\n{}\n{}\n", + "0f".repeat(8), + 100, + "-", + line("carol@example.org"), + line("before headers"), + line("an old body"), + ); + std::fs::write(&path, legacy).unwrap(); + let loaded = load_drafts(&path); + assert_eq!(loaded.len(), 1); + assert_eq!(loaded[0].to, "carol@example.org"); + assert!(loaded[0].headers.is_empty()); + } + + #[test] + fn header_rows_follow_the_frontmatter_rules() { + // The allowed forms: a pair, and the empty row a send skips. + assert_eq!( + header_row("Urgency: high"), + Ok(Some(("Urgency".into(), "high".into()))) + ); + assert_eq!( + header_row(" spaced-key : trimmed value "), + Ok(Some(("spaced-key".into(), "trimmed value".into()))) + ); + assert_eq!(header_row(""), Ok(None)); + assert_eq!(header_row(" "), Ok(None)); + + // Everything the reader would reject the whole block for. + assert!(header_row("no colon").is_err()); + assert!(header_row(": value").is_err()); + assert!(header_row("bad key: v").is_err()); + assert!(header_row("ke\ty: v").is_err()); + assert!(header_row(&format!("{}: v", "k".repeat(65))).is_err()); + // A trailing newline is trimmed out of the key; an inner one is + // not, and the row is rejected. + assert_eq!( + header_row("value\n: v"), + Ok(Some(("value".into(), "v".into()))) + ); + assert!(header_row("multi\nline: v").is_err()); + // The client's own fields would be shadowed by a copy. + for reserved in [ + "Subject: x", + "subject: x", + "Reply-To: x", + "In-Reply-To: x", + "accept: x", + ] { + assert!(header_row(reserved).is_err(), "{reserved}"); + } + + // Across rows: keys are case-insensitive and must be unique. + assert_eq!( + validate_headers(&[("Urgency".into(), "high".into())]), + Ok(()) + ); + assert!(validate_headers(&[ + ("Urgency".into(), "high".into()), + ("urgency".into(), "low".into()), + ]) + .is_err()); + // And the whole block stays inside the reader's budget. + let big = "v".repeat(1024); + let many: Vec<(String, String)> = + (0..5).map(|i| (format!("X-Key{i}"), big.clone())).collect(); + assert!(validate_headers(&many).is_err()); } fn master() -> [u8; KEY_LEN] { diff --git a/src/view.rs b/src/view.rs index 03416ec..e2a794a 100644 --- a/src/view.rs +++ b/src/view.rs @@ -202,6 +202,7 @@ pub struct CarrierWidgets { // compose page compose_to: adw::EntryRow, + compose_headers: gtk::ListBox, compose_subject: adw::EntryRow, compose_body: gtk::TextBuffer, compose_send: adw::ButtonContent, @@ -531,6 +532,14 @@ impl CarrierWidgets { compose_fields.append(&compose_to); compose_fields.append(&compose_subject); compose.append(&compose_fields); + // Frontmatter rows: one `Key: value` entry each, plus a flat + // add button below. Rows are rebuilt in render on structural + // changes only, so the entries never lose focus while typing. + let compose_headers = boxed_list(); + compose.append(&compose_headers); + let add_header = button("+ header", "quiet"); + add_header.set_halign(Align::Start); + compose.append(&add_header); let compose_view = gtk::TextView::new(); compose_view.set_wrap_mode(WrapMode::WordChar); compose_view.set_top_margin(8); @@ -890,6 +899,8 @@ impl CarrierWidgets { gtk::glib::Propagation::Stop }); + add_header.connect_clicked(input(Message::ComposeHeaderAdded)); + compose_send_button.connect_clicked(input(Message::SendPressed)); compose_cancel.connect_clicked(input(Message::ComposeClosed)); let to_sender = sender.clone(); @@ -991,6 +1002,7 @@ impl CarrierWidgets { article_body, actions, compose_to, + compose_headers, compose_subject, compose_body, compose_send, @@ -1128,6 +1140,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { subject, body, reply_to, + headers, } => sender.spawn_command(move |out| { let res = ops::send( &store, @@ -1140,6 +1153,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { }, &body, reply_to.as_deref(), + &headers, timeout, ); let _ = out.send(OpResult::Generic(res)); @@ -1203,15 +1217,44 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender model only, so the view never fights the user. + // Prefill entries once after a reply, write or draft reopen; + // afterwards typing flows entry -> model only, so the view never + // fights the user. The fresh flag also rebuilds the header rows, + // which otherwise sync on count changes alone. + let mut compose_fresh = false; if model.compose_fresh.take() { + compose_fresh = true; if let Some(compose) = &model.compose { w.compose_to.set_text(&compose.to); w.compose_subject.set_text(&compose.subject); w.compose_body.set_text(&compose.body); } } + let rows = model.compose.as_ref().map_or(0, |c| c.headers.len()); + let mut current = 0; + let mut child = w.compose_headers.first_child(); + while let Some(item) = child { + current += 1; + child = item.next_sibling(); + } + if compose_fresh || current != rows { + let added = rows > current; + clear_list(&w.compose_headers); + if let Some(compose) = &model.compose { + let mut last_entry = None; + for (index, text) in compose.headers.iter().enumerate() { + let (row, entry) = compose_header_row(index, text, sender); + last_entry = Some(entry); + w.compose_headers.append(&row); + } + // A freshly appended row is the one being typed into. + if added { + if let Some(entry) = last_entry { + entry.grab_focus(); + } + } + } + } // Header and sidebar identity. let address = model @@ -1711,6 +1754,57 @@ fn mail_row(folder: Folder, row: &Row) -> adw::ActionRow { item } +/// One compose header row: a `Key: value` entry plus a remove button. +/// The row carries its own validation state — anything the send would +/// reject is tinted and explained right where it is typed, and the +/// empty rows the send skips are left quiet. +fn compose_header_row<'a>( + index: usize, + text: &str, + sender: &ComponentSender, +) -> (gtk::ListBoxRow, gtk::Entry) { + let entry = gtk::Entry::new(); + entry.set_hexpand(true); + entry.set_placeholder_text(Some("Key: value")); + entry.set_text(text); + let style = |entry: >k::Entry, text: &str| match crate::ops::header_row(text) { + Err(reason) => { + entry.add_css_class("error"); + entry.set_tooltip_text(Some(&reason)); + } + Ok(_) => { + entry.remove_css_class("error"); + entry.set_tooltip_text(None); + } + }; + style(&entry, text); + let change_sender = sender.clone(); + let style_entry = entry.clone(); + entry.connect_changed(move |changed| { + let text = changed.text().to_string(); + style(&style_entry, &text); + change_sender.input(Message::ComposeHeaderChanged(index, text)); + }); + + let remove = icon_button("user-trash-symbolic", "Remove this header"); + let remove_sender = sender.clone(); + remove.connect_clicked(move |_| { + remove_sender.input(Message::ComposeHeaderRemoved(index)); + }); + + let line = gtk::Box::new(Orientation::Horizontal, 6); + line.set_margin_top(6); + line.set_margin_bottom(6); + line.set_margin_start(12); + line.set_margin_end(6); + line.append(&entry); + line.append(&remove); + + let row = gtk::ListBoxRow::new(); + row.set_child(Some(&line)); + (row, entry) +} + /// One contact in the sidebar's bottom panel: the address over its trust /// state; activating it opens the detail pane. fn sidebar_contact_row(contact: &Contact) -> adw::ActionRow { From dacbc9de382595a3bacfe69ee04de1c23b741ff9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 3 Oct 2026 17:30:22 +0300 Subject: [PATCH 05/37] feat: show received frontmatter fields in the message detail --- README.md | 2 +- src/app.rs | 1 + src/demo.rs | 24 +++++++++++++++++++++ src/ops.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/view.rs | 18 ++++++++++++++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7682a83..1178d6f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A minimalist desktop client for [Smol Mail](../smolmail): one compact window — - Pin a server's key — paste the operator's, or connect and pin what it presents — then register a username or recall an existing address. - Fetch (with cancel), read, reply and send sealed mail; the inbox, the requests tier and sent copies each keep their own folder. - Composes closed by any sidebar trip are stashed as drafts — kept beside the store, reopened from a folder that appears only once one exists; Cancel is the only way to throw one away. -- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. +- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body. - Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. - Contacts with verified/tofu marks and smol:// import; your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. - Key rotation with a confirm-first step, server pin management, a network timeout, and gsmol backup export/import that never contains the seed. diff --git a/src/app.rs b/src/app.rs index b58c462..82e0a11 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1086,6 +1086,7 @@ mod tests { body: String::new(), sender: String::new(), reply_to: None, + headers: Vec::new(), kept: false, } } diff --git a/src/demo.rs b/src/demo.rs index f74840e..9c0bfba 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -158,6 +158,30 @@ fn plant(dir: &Path) -> Result<(), String> { true, )?; + // one inbox mail with the sender's own frontmatter fields, so the + // detail view has a custom header block to show + { + let sender = identity(11); + let uri = Address::parse("alice@example.org") + .unwrap() + .uri(&sender.pk()); + let text = build_frontmatter( + &[ + ("Subject", "canary rollout report"), + ("Reply-To", &uri), + ("Priority", "high"), + ("X-Stage", "canary at 5%"), + ], + "The custom fields on this one are the sender's own — carrier shows them between the meta line and the body.", + ); + let envelope = seal(&sender, &pk, text.as_bytes(), now() - 12 * 3600, true) + .map_err(|e| e.to_string())?; + let id = message_id(&envelope); + store + .store_inbox(&id, &envelope, now() - 12 * 3600, false, false) + .map_err(|e| e.to_string())?; + } + // requests: senders not in the contact list deliver( &identity(31), diff --git a/src/ops.rs b/src/ops.rs index eb9f417..c1bd6bb 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -37,6 +37,9 @@ pub struct OpenedMsg { pub sender: String, /// The sender's self-certifying address, when the payload carried one. pub reply_to: Option, + /// The frontmatter fields carrier does not already show or act on, + /// in the order the reader's map keeps them (sorted by key). + pub headers: Vec<(String, String)>, pub kept: bool, } @@ -669,6 +672,17 @@ pub fn open( .find(|m| m.id == raw) .ok_or_else(|| "message not found".to_string())?; let described = client::describe(store, &me, &stored).map_err(explain)?; + // The subject sits in the header, the reply address powers Reply and + // the accept/block actions, and the Accept token is consumed by the + // contact flow — everything else the sender put in the frontmatter + // is theirs to say and ours to show. + const CONSUMED: [&str; 3] = ["subject", "reply-to", "accept"]; + let headers = described + .fields + .iter() + .filter(|(key, _)| !CONSUMED.contains(&key.as_str())) + .map(|(key, value)| (key.clone(), value.clone())) + .collect(); Ok(OpenedMsg { id: id.to_string(), from: match folder { @@ -680,6 +694,7 @@ pub fn open( body: described.text, sender: b32(&described.sender), reply_to: described.fields.get("reply-to").cloned(), + headers, kept: stored.kept, }) } @@ -888,7 +903,12 @@ mod tests { store.set_account(&addr).unwrap(); let body = build_frontmatter( - &[("Subject", "hello"), ("Reply-To", &addr.uri(&me.me().pk()))], + &[ + ("Subject", "hello"), + ("Reply-To", &addr.uri(&me.me().pk())), + ("Urgency", "high"), + ("X-Source", "nightly build"), + ], "the body text", ); let envelope = seal(me.me(), &me.me().pk(), body.as_bytes(), now(), true).unwrap(); @@ -910,6 +930,15 @@ mod tests { assert_eq!(opened.subject, "hello"); assert_eq!(opened.body, "the body text"); assert!(opened.reply_to.is_some()); + // The sender's own fields come through; the ones carrier already + // consumes do not. + assert_eq!( + opened.headers, + [ + ("urgency".to_string(), "high".to_string()), + ("x-source".to_string(), "nightly build".to_string()), + ] + ); let mut read = HashSet::new(); read.insert(id_hex); @@ -975,6 +1004,35 @@ mod tests { assert!(pins(&store).unwrap().is_empty()); } + /// The dev-build demo seed grows a mail that carries the sender's own + /// frontmatter, and it opens with those fields intact — the case the + /// message detail's header block is laid out around. + #[cfg(debug_assertions)] + #[test] + fn demo_seed_carries_a_mail_with_custom_fields() { + let dir = tempfile::tempdir().unwrap(); + crate::demo::seed(dir.path()); + let store = Store::open(&dir.path().join("carrier.db")).unwrap(); + // The demo master is fixed bytes in demo.rs. + let master: [u8; KEY_LEN] = core::array::from_fn(|i| i as u8 + 1); + let empty = HashSet::new(); + + let folders = lists(&store, &master, &empty).unwrap(); + let row = folders[0] + .iter() + .find(|row| row.subject == "canary rollout report") + .expect("the custom-fields mail in the demo inbox"); + + let opened = open(&store, &master, &row.id, Folder::Inbox).unwrap(); + assert_eq!( + opened.headers, + [ + ("priority".to_string(), "high".to_string()), + ("x-stage".to_string(), "canary at 5%".to_string()), + ] + ); + } + #[test] fn stamps_are_utc_civil_dates() { assert_eq!(when(0), "1970-01-01 00:00"); diff --git a/src/view.rs b/src/view.rs index e2a794a..9d1d57e 100644 --- a/src/view.rs +++ b/src/view.rs @@ -196,6 +196,7 @@ pub struct CarrierWidgets { article_stack: gtk::Stack, article_subject: gtk::Label, article_meta: gtk::Label, + article_headers: gtk::Label, article_view: gtk::TextView, article_body: gtk::TextBuffer, actions: gtk::Box, @@ -470,6 +471,15 @@ impl CarrierWidgets { article_meta.set_wrap(true); article_head.append(&subject_row); article_head.append(&article_meta); + // The sender's own frontmatter fields, when a mail carries any: + // envelope data, so it reads like the contact detail's key block + // rather than like body prose. + let article_headers = label("", "caption mono"); + article_headers.set_wrap(true); + article_headers.set_wrap_mode(gtk::pango::WrapMode::WordChar); + article_headers.set_selectable(true); + article_headers.set_visible(false); + article_head.append(&article_headers); article.append(&article_head); article.append(>k::Separator::new(Orientation::Horizontal)); let article_view = gtk::TextView::new(); @@ -998,6 +1008,7 @@ impl CarrierWidgets { article_stack, article_subject, article_meta, + article_headers, article_view, article_body, actions, @@ -1483,6 +1494,13 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender = msg + .headers + .iter() + .map(|(key, value)| format!("{key}: {value}")) + .collect(); + w.article_headers.set_visible(!headers.is_empty()); + w.article_headers.set_text(&headers.join("\n")); w.article_body.set_text(&msg.body); clear(&w.actions); From cda8985218f94592d0d3fba84ec53aaa9502cfa7 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 3 Oct 2026 17:58:30 +0300 Subject: [PATCH 06/37] feat: ask whether a reply carries the sender's frontmatter --- README.md | 2 +- src/app.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/ops.rs | 28 ++++++++++++++ src/view.rs | 39 +++++++++++++++---- 4 files changed, 167 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1178d6f..dfe0fa3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A minimalist desktop client for [Smol Mail](../smolmail): one compact window — - Pin a server's key — paste the operator's, or connect and pin what it presents — then register a username or recall an existing address. - Fetch (with cancel), read, reply and send sealed mail; the inbox, the requests tier and sent copies each keep their own folder. - Composes closed by any sidebar trip are stashed as drafts — kept beside the store, reopened from a folder that appears only once one exists; Cancel is the only way to throw one away. -- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body. +- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body; replying to such a mail asks whether to carry copies of the fields into the reply, where they stay editable. - Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. - Contacts with verified/tofu marks and smol:// import; your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. - Key rotation with a confirm-first step, server pin management, a network timeout, and gsmol backup export/import that never contains the seed. diff --git a/src/app.rs b/src/app.rs index 82e0a11..4e83f83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -107,6 +107,14 @@ pub enum Dialog { }, /// Rotation only moves the index forward, so the action asks first. Rotate, + /// The message being answered carries the sender's own frontmatter + /// fields; the reply asks whether to carry copies of them. + Reply { + to: String, + subject: String, + id: String, + rows: Vec, + }, } /// UI inputs. Background results arrive as [`OpResult`] instead, so the @@ -154,6 +162,8 @@ pub enum Message { ComposeClosed, SendPressed, ReplyPressed, + ReplyCancelled, + ReplyConfirmed(bool), DeletePressed, DeleteConfirmed(bool), DeleteCancelled, @@ -763,12 +773,49 @@ impl Carrier { } else { format!("Re: {}", msg.subject) }; + // The sender's own fields are the one thing a reply has to + // decide about: carry copies, or start clean. Only the + // fields that would survive send-time validation ask — + // a mail whose only extra field is the threading id + // replies straight through. + let rows = ops::reply_rows(&msg.headers); + if rows.is_empty() { + self.compose = Some(Compose { + to, + subject, + body: String::new(), + reply_to: Some(msg.id.clone()), + headers: Vec::new(), + }); + self.compose_fresh.set(true); + } else { + self.dialog = Some(Dialog::Reply { + to, + subject, + id: msg.id.clone(), + rows, + }); + } + } + Message::ReplyCancelled => { + self.dialog = None; + } + Message::ReplyConfirmed(copy) => { + let Some(Dialog::Reply { + to, + subject, + id, + rows, + }) = self.dialog.take() + else { + return Vec::new(); + }; self.compose = Some(Compose { to, subject, body: String::new(), - reply_to: Some(msg.id.clone()), - headers: Vec::new(), + reply_to: Some(id), + headers: if copy { rows } else { Vec::new() }, }); self.compose_fresh.set(true); } @@ -1245,6 +1292,63 @@ mod tests { )); } + #[test] + fn replying_to_custom_fields_asks_what_to_carry() { + let mut app = app(); + app.open = Some(( + Folder::Inbox, + OpenedMsg { + subject: "hello".into(), + headers: vec![ + ("priority".to_string(), "high".to_string()), + ("in-reply-to".to_string(), "cd".repeat(32)), + ], + ..opened() + }, + )); + + // The ask: a dialog, not a compose yet, and cancelling aborts. + app.handle(Message::ReplyPressed); + assert!(app.compose.is_none()); + assert!(matches!(app.dialog, Some(Dialog::Reply { .. }))); + app.handle(Message::ReplyCancelled); + assert!(app.dialog.is_none()); + assert!(app.compose.is_none()); + + // Either answer opens the same reply; the fields follow the choice. + app.handle(Message::ReplyPressed); + app.handle(Message::ReplyConfirmed(false)); + let compose = app.compose.as_ref().expect("reply open"); + assert_eq!(compose.subject, "Re: hello"); + assert!(compose.headers.is_empty()); + assert_eq!(compose.reply_to.as_deref(), Some("ab".repeat(32).as_str())); + + app.compose = None; + app.handle(Message::ReplyPressed); + app.handle(Message::ReplyConfirmed(true)); + let compose = app.compose.as_ref().expect("reply open"); + // The sender's field arrives; the threading id does not — the + // reply carries its own. + assert_eq!(compose.headers, ["priority: high"]); + } + + #[test] + fn a_thread_only_mail_replies_straight_through() { + let mut app = app(); + app.open = Some(( + Folder::Inbox, + OpenedMsg { + subject: "hello".into(), + headers: vec![("in-reply-to".to_string(), "cd".repeat(32))], + ..opened() + }, + )); + app.handle(Message::ReplyPressed); + assert!(app.dialog.is_none()); + assert!(app.compose.is_some()); + assert!(app.compose.as_ref().unwrap().headers.is_empty()); + } + #[test] fn headers_survive_a_stash_and_reopen() { let mut app = app(); diff --git a/src/ops.rs b/src/ops.rs index c1bd6bb..5cf1d66 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -462,6 +462,18 @@ pub fn validate_headers(headers: &[(String, String)]) -> Result<(), String> { Ok(()) } +/// The frontmatter rows a reply could carry over from the message it +/// answers. The reserved fields never make it — `in-reply-to` in +/// particular shows in the detail but belongs to the reply context the +/// client sets itself — so anything left is the sender's own metadata. +pub fn reply_rows(headers: &[(String, String)]) -> Vec { + headers + .iter() + .filter_map(|(key, value)| header_row(&format!("{key}: {value}")).ok().flatten()) + .map(|(key, value)| format!("{key}: {value}")) + .collect() +} + pub fn send( store: &Store, master: &[u8; KEY_LEN], @@ -863,6 +875,22 @@ mod tests { assert!(validate_headers(&many).is_err()); } + #[test] + fn reply_rows_drop_what_a_reply_sets_itself() { + // Only the sender's own, writable fields carry over; the reserved + // ones — threading included — are the reply's own context. + let rows = reply_rows(&[ + ("priority".to_string(), "high".to_string()), + ("in-reply-to".to_string(), "ab".repeat(32)), + ("subject".to_string(), "shadow".to_string()), + ("accept".to_string(), "token".to_string()), + ]); + assert_eq!(rows, ["priority: high"]); + // A mail whose only extra field is the threading id carries + // nothing to ask about. + assert!(reply_rows(&[("in-reply-to".to_string(), "ab".repeat(32))]).is_empty()); + } + fn master() -> [u8; KEY_LEN] { let mut seed = [0u8; KEY_LEN]; for (i, b) in seed.iter_mut().enumerate() { diff --git a/src/view.rs b/src/view.rs index 9d1d57e..90f059a 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1649,6 +1649,7 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let rotating = matches!(dialog, Dialog::Rotate); + let replying = matches!(dialog, Dialog::Reply { .. }); let alert = match dialog { Dialog::Delete { kept, .. } => { let alert = adw::AlertDialog::new( @@ -1681,17 +1682,41 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { + let list = rows + .iter() + .map(String::as_str) + .collect::>() + .join("\n"); + let alert = adw::AlertDialog::new( + Some("Carry the sender's fields into the reply?"), + Some(&format!( + "This message carries its own frontmatter:\n\n{list}\n\nReply with copies of these fields, or start with none. The fields stay editable either way." + )), + ); + alert.add_response("cancel", "Cancel Reply"); + alert.add_response("without", "Reply Without"); + alert.add_response("copy", "Copy Fields"); + alert.set_response_appearance("copy", adw::ResponseAppearance::Suggested); + alert + } }; + // The reply dialog carries its own default: Enter answers the + // question instead of cancelling the reply the user asked for. + let close_default = if replying { "without" } else { "cancel" }; alert.set_close_response("cancel"); - alert.set_default_response(Some("cancel")); + alert.set_default_response(Some(close_default)); let response_sender = sender.clone(); alert.connect_response(None, move |_, response| { - response_sender.input(match (rotating, response) { - (true, "rotate") => Message::RotateConfirmed, - (true, _) => Message::RotateCancelled, - (false, "here") => Message::DeleteConfirmed(false), - (false, "everywhere") => Message::DeleteConfirmed(true), - (false, _) => Message::DeleteCancelled, + response_sender.input(match (rotating, replying, response) { + (true, _, "rotate") => Message::RotateConfirmed, + (true, _, _) => Message::RotateCancelled, + (_, true, "copy") => Message::ReplyConfirmed(true), + (_, true, "without") => Message::ReplyConfirmed(false), + (_, true, _) => Message::ReplyCancelled, + (false, _, "here") => Message::DeleteConfirmed(false), + (false, _, "everywhere") => Message::DeleteConfirmed(true), + (false, _, _) => Message::DeleteCancelled, }); }); alert.present(Some(&w.window)); From 402e108634ae71a228a91ac55aa9ab6223d5226f Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 13:11:09 +0300 Subject: [PATCH 07/37] feat: add macOS CI workflow targeting the Forgejo runner --- .forgejo/workflows/macos.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .forgejo/workflows/macos.yml diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml new file mode 100644 index 0000000..b0824e3 --- /dev/null +++ b/.forgejo/workflows/macos.yml @@ -0,0 +1,14 @@ +name: macOS + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + check: + runs-on: macos-arm64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + - run: devbox run check From 69fd6d58e73fea4cb7901a82d3386bc6f4fbce5c Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 13:34:00 +0300 Subject: [PATCH 08/37] feat: clear the Forgejo-injected GITHUB_TOKEN before devbox run check --- .forgejo/workflows/macos.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml index b0824e3..7aad994 100644 --- a/.forgejo/workflows/macos.yml +++ b/.forgejo/workflows/macos.yml @@ -11,4 +11,12 @@ jobs: runs-on: macos-arm64 steps: - uses: https://code.forgejo.org/actions/checkout@v4 + # Forgejo auto-injects GITHUB_TOKEN (GH Actions compatibility), scoped + # to this Forgejo instance. Nix auto-detects that env var and assumes + # it's a github.com credential, so it sends it to api.github.com when + # fetching nixpkgs tarballs for devbox - which rejects it with 401. + # Clearing it here falls back to unauthenticated (fine for public + # nixpkgs fetches, same as this succeeds locally with no token set). - run: devbox run check + env: + GITHUB_TOKEN: "" From 5d68f3e24543b737582911f88c4097b8b0666dbb Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 13:45:14 +0300 Subject: [PATCH 09/37] feat: checkout fumi as a sibling dir in CI to match the local path dependency --- .forgejo/workflows/macos.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml index 7aad994..b055c89 100644 --- a/.forgejo/workflows/macos.yml +++ b/.forgejo/workflows/macos.yml @@ -11,6 +11,15 @@ jobs: runs-on: macos-arm64 steps: - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + # Cargo.toml depends on fumi-core via a relative sibling path + # (../fumi/core), matching how it's checked out locally. Fetched as + # its own sibling checkout here so that path resolves the same way. + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + path: fumi # Forgejo auto-injects GITHUB_TOKEN (GH Actions compatibility), scoped # to this Forgejo instance. Nix auto-detects that env var and assumes # it's a github.com credential, so it sends it to api.github.com when @@ -18,5 +27,6 @@ jobs: # Clearing it here falls back to unauthenticated (fine for public # nixpkgs fetches, same as this succeeds locally with no token set). - run: devbox run check + working-directory: carrier env: GITHUB_TOKEN: "" From 648f041cf3127845bb1112e38a5b133aa2a7fb6f Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 13:46:37 +0300 Subject: [PATCH 10/37] fix: pin fumi checkout to main, skipping a default-branch API lookup the carrier-scoped token can't make --- .forgejo/workflows/macos.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml index b055c89..73fb04e 100644 --- a/.forgejo/workflows/macos.yml +++ b/.forgejo/workflows/macos.yml @@ -19,6 +19,7 @@ jobs: - uses: https://code.forgejo.org/actions/checkout@v4 with: repository: randogoth/fumi + ref: main path: fumi # Forgejo auto-injects GITHUB_TOKEN (GH Actions compatibility), scoped # to this Forgejo instance. Nix auto-detects that env var and assumes From 2447e9bcb52d0adc4e92d68bef587cd36acdef80 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 15:30:17 +0300 Subject: [PATCH 11/37] feat: cache cargo registry and target dirs keyed on Cargo.lock --- .forgejo/workflows/macos.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/macos.yml index 73fb04e..96a8439 100644 --- a/.forgejo/workflows/macos.yml +++ b/.forgejo/workflows/macos.yml @@ -21,6 +21,14 @@ jobs: repository: randogoth/fumi ref: main path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo- # Forgejo auto-injects GITHUB_TOKEN (GH Actions compatibility), scoped # to this Forgejo instance. Nix auto-detects that env var and assumes # it's a github.com credential, so it sends it to api.github.com when From ba97f63841843bb20514cbe7f1310b4b81d3546c Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 19:45:20 +0300 Subject: [PATCH 12/37] feat: make devbox.json cross-platform for GTK4/libadwaita Linux now gets gtk4/libadwaita declared as nix devbox packages (scoped to linux platforms, with the dev output requested explicitly - nixpkgs splits runtime libs from headers/pkgconfig, and devbox only installs the default output unless told otherwise). macOS keeps using Homebrew's build exactly as before (nixpkgs' GTK build is much rockier there). PKG_CONFIG_PATH moved out of the static env block (which can't be platform-conditional) into init_hook, which can branch on uname. Verified building clean with 0 warnings on both this machine (Linux) and apfel (macOS). --- devbox.json | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/devbox.json b/devbox.json index be95b18..7a970a7 100644 --- a/devbox.json +++ b/devbox.json @@ -1,18 +1,29 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", - "packages": [ - "rustc@latest", - "cargo@latest", - "pkg-config@latest", - "rustfmt@latest" - ], + "packages": { + "rustc": "latest", + "cargo": "latest", + "pkg-config": "latest", + "rustfmt": "latest", + "gtk4": { + "version": "latest", + "platforms": ["x86_64-linux", "aarch64-linux"], + "outputs": ["out", "dev"] + }, + "libadwaita": { + "version": "latest", + "platforms": ["x86_64-linux", "aarch64-linux"], + "outputs": ["out", "dev"] + } + }, "env": { - "PATH": "/opt/homebrew/bin:/opt/homebrew/sbin:$PATH", - "PKG_CONFIG_PATH": "/opt/homebrew/lib/pkgconfig", - "PKG_CONFIG_PATH_arm64_apple_darwin": "/opt/homebrew/lib/pkgconfig" + "PATH": "/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" }, "shell": { - "init_hook": [], + "init_hook": [ + "export PKG_CONFIG_PATH=\"$DEVBOX_PACKAGES_DIR/lib/pkgconfig:$PKG_CONFIG_PATH\"", + "if [[ \"$(uname)\" == \"Darwin\" ]]; then export PKG_CONFIG_PATH=\"/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH\"; export PKG_CONFIG_PATH_arm64_apple_darwin=\"/opt/homebrew/lib/pkgconfig\"; fi" + ], "scripts": { "build": [ "cargo build --release" From 15fdbe4dbdb006ad5b046da21edc1b4798ed8467 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 19:47:41 +0300 Subject: [PATCH 13/37] chore: update devbox.lock for gtk4/libadwaita on Linux --- devbox.lock | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/devbox.lock b/devbox.lock index d9c9eac..21d45a8 100644 --- a/devbox.lock +++ b/devbox.lock @@ -43,6 +43,156 @@ "last_modified": "2026-09-29T03:29:29Z", "resolved": "github:NixOS/nixpkgs/b6c8664de9b6cc07fe5666a29f91884ba81197c4?lastModified=1790652569&narHash=sha256-641r7xrlSOHYoktL9%2FaArPIYNgZJ9eM5URzSEOtPJDY%3D" }, + "gtk4@latest": { + "last_modified": "2026-08-28T08:26:42Z", + "resolved": "github:NixOS/nixpkgs/0e7911a191ea0fb9d6e8e771d24736d3618f8b1f#gtk4", + "source": "devbox-search", + "version": "4.22.4", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zip9cgwsmacrd8rv9x8j6v5d93fyxb6d-gtk4-4.22.4", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/34wj0s7jwk37sfxaw1vljl8aq1v43mxg-gtk4-4.22.4-dev" + } + ], + "store_path": "/nix/store/zip9cgwsmacrd8rv9x8j6v5d93fyxb6d-gtk4-4.22.4" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/x2y5vykblarp5jzs7n85sh5gixi4r7hx-gtk4-4.22.4", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/cgr4b28yx7zzdxkjifwba2v5f9zprh8p-gtk4-4.22.4-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/qpm15s7r2g7qwrr0fnjpgl4jkvh820x9-gtk4-4.22.4-devdoc" + }, + { + "name": "debug", + "path": "/nix/store/hkpjzp8nah9cj5gmp51ydh658rbnh9s8-gtk4-4.22.4-debug" + } + ], + "store_path": "/nix/store/x2y5vykblarp5jzs7n85sh5gixi4r7hx-gtk4-4.22.4" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/3la8zml5yi34lssl8j3g1w7n2qs2j40w-gtk4-4.22.4", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/8vgk08hbgzswqrf6nbm0m7w5qxzi5j7i-gtk4-4.22.4-dev" + } + ], + "store_path": "/nix/store/3la8zml5yi34lssl8j3g1w7n2qs2j40w-gtk4-4.22.4" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4j905cjgk8idkzg4szhyryzjx2q1b6br-gtk4-4.22.4", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/r7hhl6pbzld4m34qza3xr08xmm13ssmk-gtk4-4.22.4-debug" + }, + { + "name": "dev", + "path": "/nix/store/dczin2m9wvwsfjyfbnksm983ljgmibfk-gtk4-4.22.4-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/7jwx0qmwvx9w2z5b5m5fw2d9k2d1a3wd-gtk4-4.22.4-devdoc" + } + ], + "store_path": "/nix/store/4j905cjgk8idkzg4szhyryzjx2q1b6br-gtk4-4.22.4" + } + } + }, + "libadwaita@latest": { + "last_modified": "2026-08-29T00:50:12Z", + "resolved": "github:NixOS/nixpkgs/e8be7818e19ada32105a8af937a6a473b38167ca#libadwaita", + "source": "devbox-search", + "version": "1.9.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/dcz2cyp4bzlbbwrq0z9rpaijrhbm1nv2-libadwaita-1.9.3", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/sqp0z2gcbgpzs9gcfrs7jzpy1v1k5qv0-libadwaita-1.9.3-devdoc" + }, + { + "name": "dev", + "path": "/nix/store/a39iqc19qfbkki6ibsylh8kard0yajhy-libadwaita-1.9.3-dev" + } + ], + "store_path": "/nix/store/dcz2cyp4bzlbbwrq0z9rpaijrhbm1nv2-libadwaita-1.9.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/czi84wxq4v7vl7k4dr59nrwwk91pc5d4-libadwaita-1.9.3", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/yg7bgsvyyp8ky0mbgr8hm6szixcw71bs-libadwaita-1.9.3-debug" + }, + { + "name": "dev", + "path": "/nix/store/hss38kikfq0v5cd5hi353qj5smnhwiga-libadwaita-1.9.3-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/rrkay1bb7zyicqb76n9ynk695bzra7l0-libadwaita-1.9.3-devdoc" + } + ], + "store_path": "/nix/store/czi84wxq4v7vl7k4dr59nrwwk91pc5d4-libadwaita-1.9.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/x8v08wl57vkyk6qfcblx32a0w6cphnpy-libadwaita-1.9.3", + "default": true + }, + { + "name": "devdoc", + "path": "/nix/store/4bi0mlap06fcw0ycsd0yslyzk57lir9r-libadwaita-1.9.3-devdoc" + }, + { + "name": "debug", + "path": "/nix/store/l4h0wjg04i4bdz5xvafchxw9hh1a9dc1-libadwaita-1.9.3-debug" + }, + { + "name": "dev", + "path": "/nix/store/0gvpb9gszkl274wgb3zgcwlclbwj6cch-libadwaita-1.9.3-dev" + } + ], + "store_path": "/nix/store/x8v08wl57vkyk6qfcblx32a0w6cphnpy-libadwaita-1.9.3" + } + } + }, "pkg-config@latest": { "last_modified": "2026-10-05T09:07:57Z", "resolved": "github:NixOS/nixpkgs/aa48d347080940b8a2b8d2f48228674e280a3514#pkg-config", From d85427156e1647afde69a71fc2629d7cc539f5b6 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 19:47:41 +0300 Subject: [PATCH 14/37] feat: add a Linux CI job alongside macOS, in one shared workflow Renamed macos.yml to ci.yml with two jobs (macos, linux) under one push/PR/dispatch trigger, so both run from the same commit instead of needing separate files. Cache keys are now OS-prefixed (cargo-macos-/ cargo-linux-): target/ artifacts are platform-specific and would otherwise collide under a shared key. --- .forgejo/workflows/{macos.yml => ci.yml} | 35 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) rename .forgejo/workflows/{macos.yml => ci.yml} (56%) diff --git a/.forgejo/workflows/macos.yml b/.forgejo/workflows/ci.yml similarity index 56% rename from .forgejo/workflows/macos.yml rename to .forgejo/workflows/ci.yml index 96a8439..cd0dd1a 100644 --- a/.forgejo/workflows/macos.yml +++ b/.forgejo/workflows/ci.yml @@ -1,4 +1,4 @@ -name: macOS +name: CI on: push: @@ -7,7 +7,7 @@ on: workflow_dispatch: jobs: - check: + macos: runs-on: macos-arm64 steps: - uses: https://code.forgejo.org/actions/checkout@v4 @@ -21,14 +21,17 @@ jobs: repository: randogoth/fumi ref: main path: fumi + # Cache key includes the OS explicitly: target/ artifacts are + # platform-specific, so macOS and Linux must never share a cache key + # or they'd clobber each other's compiled objects. - uses: https://code.forgejo.org/actions/cache@v4 with: path: | ~/.cargo/registry carrier/target fumi/target - key: cargo-${{ hashFiles('carrier/Cargo.lock') }} - restore-keys: cargo- + key: cargo-macos-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-macos- # Forgejo auto-injects GITHUB_TOKEN (GH Actions compatibility), scoped # to this Forgejo instance. Nix auto-detects that env var and assumes # it's a github.com credential, so it sends it to api.github.com when @@ -39,3 +42,27 @@ jobs: working-directory: carrier env: GITHUB_TOKEN: "" + + linux: + runs-on: linux-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + ref: main + path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-linux-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-linux- + - run: devbox run check + working-directory: carrier + env: + GITHUB_TOKEN: "" From 73408bdf2458296deeee17fe7a30203218cfc1e9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 20:03:18 +0300 Subject: [PATCH 15/37] Add tag-triggered release workflow Builds macos-arm64 and linux-x86_64 release binaries in parallel and uploads them as build artifacts; a single release job gated on both then downloads and publishes them to one Forgejo release. Splitting it this way avoids a real race in actions/forgejo-release itself: its script always does a blind create-then-upload for the tag with no merge path, so two parallel callers against the same tag either hard-fail or stomp each other's uploaded assets. --- .forgejo/workflows/release.yml | 97 ++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .forgejo/workflows/release.yml diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..1b2b26b --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,97 @@ +name: Release + +on: + push: + tags: ["v*"] + +jobs: + macos: + runs-on: macos-arm64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + ref: main + path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-macos-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-macos- + - run: devbox run build + working-directory: carrier + env: + GITHUB_TOKEN: "" + - run: mv target/release/carrier target/release/carrier-macos-arm64 + working-directory: carrier + # archive: false uploads the raw binary (not a zip) and names the + # artifact after the file, so this rename is what keeps the two + # platforms' artifacts from colliding under the same "carrier" name. + - uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: carrier-macos-arm64 + path: carrier/target/release/carrier-macos-arm64 + archive: false + + linux: + runs-on: linux-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + ref: main + path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-linux-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-linux- + - run: devbox run build + working-directory: carrier + env: + GITHUB_TOKEN: "" + - run: mv target/release/carrier target/release/carrier-linux-x86_64 + working-directory: carrier + - uses: https://code.forgejo.org/actions/upload-artifact@v4 + with: + name: carrier-linux-x86_64 + path: carrier/target/release/carrier-linux-x86_64 + archive: false + + # forgejo-release.sh (the engine behind actions/forgejo-release) always + # does a blind create-then-upload of the release for the tag, with no + # check for one already existing - running it from two parallel jobs + # against the same tag either hard-fails the second create (override: + # false) or races a delete-and-recreate that can wipe out the first job's + # just-uploaded assets (override: true). So platform jobs only ever + # produce artifacts; exactly one job, gated on both finishing, does the + # single release create + upload. + release: + needs: [macos, linux] + runs-on: linux-x86_64 + steps: + - uses: https://code.forgejo.org/actions/download-artifact@v4 + with: + path: dist/release + merge-multiple: true + - uses: https://code.forgejo.org/actions/forgejo-release@v2.13.4 + with: + direction: upload + release-dir: dist/release + # Safe here specifically because this is the only job that ever + # calls forgejo-release for a given tag: re-running this same job + # for the same tag (e.g. a manual rerun) replaces its release + # cleanly instead of failing on "already exists". + override: true From 1bbc961bd68ce2da1737041425ed1832791691ba Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 20:07:09 +0300 Subject: [PATCH 16/37] Fix release workflow: use Forgejo's upload/download-artifact fork actions/upload-artifact@v4 and download-artifact@v4 use GitHub's newer Artifacts-v2 protocol, which Forgejo's server explicitly refuses ("not currently supported on GHES"). Forgejo maintains its own forks (forgejo/upload-artifact@v5, forgejo/download-artifact@v7) with that check disabled. Also drops archive: false, an input that fork doesn't expose - the earlier binary rename already keeps filenames distinct through the default zip/extract round-trip. --- .forgejo/workflows/release.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 1b2b26b..037c8d4 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -30,14 +30,14 @@ jobs: GITHUB_TOKEN: "" - run: mv target/release/carrier target/release/carrier-macos-arm64 working-directory: carrier - # archive: false uploads the raw binary (not a zip) and names the - # artifact after the file, so this rename is what keeps the two - # platforms' artifacts from colliding under the same "carrier" name. - - uses: https://code.forgejo.org/actions/upload-artifact@v4 + # actions/upload-artifact@v4 talks the newer Artifacts-v2 protocol, + # which Forgejo's server rejects ("not currently supported on GHES"). + # forgejo/upload-artifact is Forgejo's own patched fork with that + # check disabled - needed on both upload and download ends. + - uses: https://code.forgejo.org/forgejo/upload-artifact@v5 with: name: carrier-macos-arm64 path: carrier/target/release/carrier-macos-arm64 - archive: false linux: runs-on: linux-x86_64 @@ -64,11 +64,10 @@ jobs: GITHUB_TOKEN: "" - run: mv target/release/carrier target/release/carrier-linux-x86_64 working-directory: carrier - - uses: https://code.forgejo.org/actions/upload-artifact@v4 + - uses: https://code.forgejo.org/forgejo/upload-artifact@v5 with: name: carrier-linux-x86_64 path: carrier/target/release/carrier-linux-x86_64 - archive: false # forgejo-release.sh (the engine behind actions/forgejo-release) always # does a blind create-then-upload of the release for the tag, with no @@ -82,7 +81,7 @@ jobs: needs: [macos, linux] runs-on: linux-x86_64 steps: - - uses: https://code.forgejo.org/actions/download-artifact@v4 + - uses: https://code.forgejo.org/forgejo/download-artifact@v7 with: path: dist/release merge-multiple: true From 2d18caa500da412cfbee341292f23759f654398a Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 20:39:57 +0300 Subject: [PATCH 17/37] Add Flatpak bundle to the release workflow Builds carrier-x86_64.flatpak from the linux job's already-built binary rather than recompiling inside flatpak-builder's sandbox (which can't see the shared cargo cache anyway, making an in-sandbox build pure duplicate work). com.randogoth.carrier.release.json is a separate manifest from the main one, which stays as-is for local `devbox run package` dev-from-source builds. --- .forgejo/workflows/release.yml | 31 +++++++++++++++++++- .gitignore | 4 +++ com.randogoth.carrier.release.json | 45 ++++++++++++++++++++++++++++++ devbox.json | 4 +++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 com.randogoth.carrier.release.json diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 037c8d4..94b0db8 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -69,6 +69,35 @@ jobs: name: carrier-linux-x86_64 path: carrier/target/release/carrier-linux-x86_64 + # Packages the linux job's already-built binary instead of recompiling + # inside flatpak-builder's own sandbox: that sandbox copies the source + # tree in fresh, so it never sees ~/.cargo/registry or target/, making an + # in-sandbox cargo build fully redundant with the linux job's build. + # com.randogoth.carrier.release.json (not the main manifest, which is + # still used by local `devbox run package` for from-source dev builds) + # just installs this prebuilt binary instead of invoking cargo. + flatpak: + needs: [linux] + runs-on: linux-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/forgejo/download-artifact@v7 + with: + name: carrier-linux-x86_64 + path: carrier + - run: chmod +x carrier-linux-x86_64 + working-directory: carrier + - run: devbox run bundle-flatpak + working-directory: carrier + env: + GITHUB_TOKEN: "" + - uses: https://code.forgejo.org/forgejo/upload-artifact@v5 + with: + name: carrier-x86_64.flatpak + path: carrier/carrier-x86_64.flatpak + # forgejo-release.sh (the engine behind actions/forgejo-release) always # does a blind create-then-upload of the release for the tag, with no # check for one already existing - running it from two parallel jobs @@ -78,7 +107,7 @@ jobs: # produce artifacts; exactly one job, gated on both finishing, does the # single release create + upload. release: - needs: [macos, linux] + needs: [macos, linux, flatpak] runs-on: linux-x86_64 steps: - uses: https://code.forgejo.org/forgejo/download-artifact@v7 diff --git a/.gitignore b/.gitignore index 9da6713..6bc4627 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ /target .flatpak .flatpak-builder +.flatpak-repo +.flatpak-build +*.flatpak +carrier-linux-x86_64 result result-* *.key diff --git a/com.randogoth.carrier.release.json b/com.randogoth.carrier.release.json new file mode 100644 index 0000000..33716ed --- /dev/null +++ b/com.randogoth.carrier.release.json @@ -0,0 +1,45 @@ +{ + "id": "com.randogoth.carrier", + "runtime": "org.gnome.Platform", + "runtime-version": "51", + "sdk": "org.gnome.Sdk", + "command": "carrier", + "finish-args": [ + "--socket=wayland", + "--socket=fallback-x11", + "--device=dri", + "--share=network" + ], + "cleanup": [ + "/share/doc" + ], + "modules": [ + { + "name": "carrier", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 carrier-linux-x86_64 /app/bin/carrier", + "install -Dm644 data/com.randogoth.carrier.desktop /app/share/applications/com.randogoth.carrier.desktop", + "install -Dm644 data/com.randogoth.carrier.metainfo.xml /app/share/metainfo/com.randogoth.carrier.metainfo.xml", + "install -Dm644 data/icons/com.randogoth.carrier.png /app/share/icons/hicolor/512x512/apps/com.randogoth.carrier.png", + "install -Dm644 data/icons/com.randogoth.carrier-256.png /app/share/icons/hicolor/256x256/apps/com.randogoth.carrier.png", + "install -Dm644 data/icons/com.randogoth.carrier-128.png /app/share/icons/hicolor/128x128/apps/com.randogoth.carrier.png" + ], + "sources": [ + { + "type": "dir", + "path": ".", + "skip": [ + "target", + ".git", + ".jj", + ".flatpak", + ".flatpak-builder", + ".flatpak-repo", + ".flatpak-build" + ] + } + ] + } + ] +} diff --git a/devbox.json b/devbox.json index 7a970a7..6d282f6 100644 --- a/devbox.json +++ b/devbox.json @@ -42,6 +42,10 @@ ], "package": [ "flatpak-builder --force-clean --user --install-deps-from=flathub --install .flatpak com.randogoth.carrier.json" + ], + "bundle-flatpak": [ + "flatpak-builder --force-clean --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", + "flatpak build-bundle .flatpak-repo carrier-x86_64.flatpak com.randogoth.carrier" ] } } From 5d097143a5a622f256e5892b355a72d56b723d05 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 20:53:22 +0300 Subject: [PATCH 18/37] Decouple macOS from the release job apfel is a laptop that's frequently asleep/offline; gating the release on it (needs: [macos, linux, flatpak]) meant the whole release stalled whenever it wasn't reachable. macos now appends its own asset directly via the Forgejo API once it finishes, polling for the release that linux+flatpak already created/overrode, instead of going through actions/forgejo-release (which can only blind-create, not append to an existing release). --- .forgejo/workflows/release.yml | 57 ++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 94b0db8..b6eb49f 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -30,14 +30,48 @@ jobs: GITHUB_TOKEN: "" - run: mv target/release/carrier target/release/carrier-macos-arm64 working-directory: carrier - # actions/upload-artifact@v4 talks the newer Artifacts-v2 protocol, - # which Forgejo's server rejects ("not currently supported on GHES"). - # forgejo/upload-artifact is Forgejo's own patched fork with that - # check disabled - needed on both upload and download ends. - - uses: https://code.forgejo.org/forgejo/upload-artifact@v5 - with: - name: carrier-macos-arm64 - path: carrier/target/release/carrier-macos-arm64 + # This runner is a laptop that's frequently asleep/offline, so this + # job must not gate the release (and must not be gated by it either). + # Instead of going through actions/forgejo-release - which only knows + # how to blind-create a release, not append to one that may already + # exist - this polls for the release the linux+flatpak job creates + # and appends its own asset directly via the Forgejo API, landing + # next to whatever binaries are already there. grep (not jq, whose + # presence here isn't guaranteed) is safe for pulling the id out: per + # Forgejo's structs.Release, ID is the first declared field and Go's + # encoding/json marshals in declaration order, ahead of the nested + # per-asset "id" fields in the "assets" array. + - run: | + REPO="${{ forge.repository }}" + TAG="${{ forge.ref_name }}" + FORGEJO="${{ env.FORGEJO_SERVER_URL }}" + FORGEJO="${FORGEJO%%/}" + TOKEN="${{ forge.token }}" + FILE="target/release/carrier-macos-arm64" + NAME="carrier-macos-arm64" + + release_id="" + for i in $(seq 1 40); do + body=$(curl --retry 5 -sS -H "Authorization: token $TOKEN" "$FORGEJO/api/v1/repos/$REPO/releases/tags/$TAG") + release_id=$(echo "$body" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') + if [ -n "$release_id" ]; then + break + fi + echo "release for $TAG not ready yet, waiting 15s ($i/40)" + sleep 15 + done + + if [ -z "$release_id" ]; then + echo "release for $TAG never appeared after 10 minutes" >&2 + exit 1 + fi + + curl --retry 5 --fail -sS -X POST \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@$FILE" \ + "$FORGEJO/api/v1/repos/$REPO/releases/$release_id/assets?name=$NAME" + working-directory: carrier linux: runs-on: linux-x86_64 @@ -105,9 +139,12 @@ jobs: # false) or races a delete-and-recreate that can wipe out the first job's # just-uploaded assets (override: true). So platform jobs only ever # produce artifacts; exactly one job, gated on both finishing, does the - # single release create + upload. + # single release create + upload. macos is deliberately left out of + # needs (see that job): re-running this job after macos has appended its + # asset will wipe that asset out via override - rerun macos again after + # to restore it. release: - needs: [macos, linux, flatpak] + needs: [linux, flatpak] runs-on: linux-x86_64 steps: - uses: https://code.forgejo.org/forgejo/download-artifact@v7 From 3183f15da65854f1acc3c0857b80cb36342c7ba5 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 21:02:27 +0300 Subject: [PATCH 19/37] Fix flatpak-builder defaulting to system scope in bundle-flatpak Missing --user (present on the existing package script, omitted here by mistake) made flatpak-builder try to install missing deps system-wide, which this unprivileged CI user can't authorize via polkit: "Flatpak system operation GetRevokefsFd not allowed for user". --- devbox.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index 6d282f6..e115bd9 100644 --- a/devbox.json +++ b/devbox.json @@ -44,7 +44,7 @@ "flatpak-builder --force-clean --user --install-deps-from=flathub --install .flatpak com.randogoth.carrier.json" ], "bundle-flatpak": [ - "flatpak-builder --force-clean --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", + "flatpak-builder --force-clean --user --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", "flatpak build-bundle .flatpak-repo carrier-x86_64.flatpak com.randogoth.carrier" ] } From cf57009eeb95e9e6531b8eb5c7820b44d6d4b0c9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 21:14:12 +0300 Subject: [PATCH 20/37] Disable flatpak-builder's rofiles-fuse under the CI runner rofiles-fuse's mount/unmount cycle fails under the forgejo-runner systemd service ("Permission denied" / "Operation not permitted" on fusermount3) even though /dev/fuse itself is world-writable - services don't get the same mount-namespace treatment an interactive session does. --disable-rofiles-fuse is flatpak-builder's own documented workaround for exactly this class of environment (containers/CI). --- devbox.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index e115bd9..5fc10e2 100644 --- a/devbox.json +++ b/devbox.json @@ -44,7 +44,7 @@ "flatpak-builder --force-clean --user --install-deps-from=flathub --install .flatpak com.randogoth.carrier.json" ], "bundle-flatpak": [ - "flatpak-builder --force-clean --user --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", + "flatpak-builder --force-clean --user --disable-rofiles-fuse --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", "flatpak build-bundle .flatpak-repo carrier-x86_64.flatpak com.randogoth.carrier" ] } From 18befcc008b259cd3201cec6f9aec0f9c05d83fd Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 7 Oct 2026 21:26:53 +0300 Subject: [PATCH 21/37] Revert flatpak to building from source; add appstream package Reusing the linux job's binary (previous commit) doesn't work: it's dynamically linked against ~100 libraries at absolute /nix/store paths, including its own ELF interpreter, none of which exist inside the Flatpak sandbox - confirmed locally via a real install+run (execvp: No such file or directory). Reverted to building from source inside flatpak-builder's sandbox via the rust-stable SDK extension, same as the existing local `devbox run package` already does. Also fixes two things needed for that in-sandbox build to work under the forgejo-runner service (not an interactive session): - appstreamcli (used by flatpak-builder to compose AppStream metadata) wasn't on PATH - added as a devbox package (linux-only, like gtk4/libadwaita) rather than a NixOS system package, keeping it scoped to the project. - rofiles-fuse's mount/unmount cycle fails under the service context even though /dev/fuse itself is world-writable; --disable-rofiles-fuse is flatpak-builder's own documented workaround for this. Verified locally: full build, appstreamcli compose, bundle install, and the installed binary actually executing inside the sandbox. --- .forgejo/workflows/release.yml | 24 ++++++------ com.randogoth.carrier.release.json | 45 ---------------------- devbox.json | 6 ++- devbox.lock | 62 ++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 59 deletions(-) delete mode 100644 com.randogoth.carrier.release.json diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index b6eb49f..90d2cc2 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -103,26 +103,24 @@ jobs: name: carrier-linux-x86_64 path: carrier/target/release/carrier-linux-x86_64 - # Packages the linux job's already-built binary instead of recompiling - # inside flatpak-builder's own sandbox: that sandbox copies the source - # tree in fresh, so it never sees ~/.cargo/registry or target/, making an - # in-sandbox cargo build fully redundant with the linux job's build. - # com.randogoth.carrier.release.json (not the main manifest, which is - # still used by local `devbox run package` for from-source dev builds) - # just installs this prebuilt binary instead of invoking cargo. + # Builds from source inside flatpak-builder's own sandbox, via the + # rust-stable SDK extension (same as com.randogoth.carrier.json already + # does for local `devbox run package`). Reusing the linux job's binary + # was tried and reverted: that binary is dynamically linked against ~100 + # libraries at absolute /nix/store paths, including its own ELF + # interpreter - none of which exist inside the Flatpak sandbox, so it + # can never execute there regardless of how it's packaged in. flatpak: - needs: [linux] runs-on: linux-x86_64 steps: - uses: https://code.forgejo.org/actions/checkout@v4 with: path: carrier - - uses: https://code.forgejo.org/forgejo/download-artifact@v7 + - uses: https://code.forgejo.org/actions/checkout@v4 with: - name: carrier-linux-x86_64 - path: carrier - - run: chmod +x carrier-linux-x86_64 - working-directory: carrier + repository: randogoth/fumi + ref: main + path: fumi - run: devbox run bundle-flatpak working-directory: carrier env: diff --git a/com.randogoth.carrier.release.json b/com.randogoth.carrier.release.json deleted file mode 100644 index 33716ed..0000000 --- a/com.randogoth.carrier.release.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "id": "com.randogoth.carrier", - "runtime": "org.gnome.Platform", - "runtime-version": "51", - "sdk": "org.gnome.Sdk", - "command": "carrier", - "finish-args": [ - "--socket=wayland", - "--socket=fallback-x11", - "--device=dri", - "--share=network" - ], - "cleanup": [ - "/share/doc" - ], - "modules": [ - { - "name": "carrier", - "buildsystem": "simple", - "build-commands": [ - "install -Dm755 carrier-linux-x86_64 /app/bin/carrier", - "install -Dm644 data/com.randogoth.carrier.desktop /app/share/applications/com.randogoth.carrier.desktop", - "install -Dm644 data/com.randogoth.carrier.metainfo.xml /app/share/metainfo/com.randogoth.carrier.metainfo.xml", - "install -Dm644 data/icons/com.randogoth.carrier.png /app/share/icons/hicolor/512x512/apps/com.randogoth.carrier.png", - "install -Dm644 data/icons/com.randogoth.carrier-256.png /app/share/icons/hicolor/256x256/apps/com.randogoth.carrier.png", - "install -Dm644 data/icons/com.randogoth.carrier-128.png /app/share/icons/hicolor/128x128/apps/com.randogoth.carrier.png" - ], - "sources": [ - { - "type": "dir", - "path": ".", - "skip": [ - "target", - ".git", - ".jj", - ".flatpak", - ".flatpak-builder", - ".flatpak-repo", - ".flatpak-build" - ] - } - ] - } - ] -} diff --git a/devbox.json b/devbox.json index 5fc10e2..312b2c8 100644 --- a/devbox.json +++ b/devbox.json @@ -14,6 +14,10 @@ "version": "latest", "platforms": ["x86_64-linux", "aarch64-linux"], "outputs": ["out", "dev"] + }, + "appstream": { + "version": "latest", + "platforms": ["x86_64-linux", "aarch64-linux"] } }, "env": { @@ -44,7 +48,7 @@ "flatpak-builder --force-clean --user --install-deps-from=flathub --install .flatpak com.randogoth.carrier.json" ], "bundle-flatpak": [ - "flatpak-builder --force-clean --user --disable-rofiles-fuse --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.release.json", + "flatpak-builder --force-clean --user --disable-rofiles-fuse --repo=.flatpak-repo --install-deps-from=flathub .flatpak-build com.randogoth.carrier.json", "flatpak build-bundle .flatpak-repo carrier-x86_64.flatpak com.randogoth.carrier" ] } diff --git a/devbox.lock b/devbox.lock index 21d45a8..2471f13 100644 --- a/devbox.lock +++ b/devbox.lock @@ -1,6 +1,68 @@ { "lockfile_version": "1", "packages": { + "appstream@latest": { + "last_modified": "2026-08-27T23:47:58Z", + "resolved": "github:NixOS/nixpkgs/7d1a7c21a4c00ea653fc7ed5c083d261340f185f#appstream", + "source": "devbox-search", + "version": "1.1.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zrpxip93379i7iphgi9r12vfkmjpln8k-appstream-1.1.3", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/c3svyp4qcd4kwscjjhba6r5ajm2kfr0q-appstream-1.1.3-dev" + }, + { + "name": "installedTests", + "path": "/nix/store/2b6x9xz39605jg62jjvcc2d6iszs5ylj-appstream-1.1.3-installedTests" + } + ], + "store_path": "/nix/store/zrpxip93379i7iphgi9r12vfkmjpln8k-appstream-1.1.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/66jhy5isw71z0xmwsva5wbj0cl65b04j-appstream-1.1.3", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/akhbgsjmqkwcfp219bv3mszmfhjvv3gl-appstream-1.1.3-dev" + }, + { + "name": "installedTests", + "path": "/nix/store/xvr3ldwd4vk8fiky1lbbq8fp3a6kyzms-appstream-1.1.3-installedTests" + } + ], + "store_path": "/nix/store/66jhy5isw71z0xmwsva5wbj0cl65b04j-appstream-1.1.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/i2azliw8fyn0dv751hkwcjp25w0k3raq-appstream-1.1.3", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/xxw2gcxw4fjhrz7flkrmsc9n75fnadcy-appstream-1.1.3-dev" + }, + { + "name": "installedTests", + "path": "/nix/store/4gbbz6ip5lmblfsl33xdxhrdpj9mgzk7-appstream-1.1.3-installedTests" + } + ], + "store_path": "/nix/store/i2azliw8fyn0dv751hkwcjp25w0k3raq-appstream-1.1.3" + } + } + }, "cargo@latest": { "last_modified": "2026-08-27T07:16:00Z", "resolved": "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410#cargo", From 53c9c4b8ceed61991788c438149c3d48b925e205 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 19:58:48 +0300 Subject: [PATCH 22/37] docs: mention factory reset in the feature list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfe0fa3..529485a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A minimalist desktop client for [Smol Mail](../smolmail): one compact window — - Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body; replying to such a mail asks whether to carry copies of the fields into the reply, where they stay editable. - Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. - Contacts with verified/tofu marks and smol:// import; your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. -- Key rotation with a confirm-first step, server pin management, a network timeout, and gsmol backup export/import that never contains the seed. +- Key rotation with a confirm-first step, server pin management, a network timeout, gsmol backup export/import that never contains the seed, and a factory reset that erases the local files and returns to onboarding. ## Layout From 2892ea3fa7e13fbc25a4b684bde729df1e13f5b9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 19:58:50 +0300 Subject: [PATCH 23/37] feat: rework signup into a safer multi-step flow, add keep-mail setting --- src/app.rs | 382 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/ops.rs | 45 ++++--- src/view.rs | 354 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 649 insertions(+), 132 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4e83f83..3b9c479 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,14 +75,28 @@ pub enum OnboardMode { /// Signup: bind the identity to a username, or recall an existing address. pub struct Signup { + pub step: SignupStep, + pub mode: SignupMode, pub user: String, pub host: String, pub invite: String, - /// The server's base32 static key, pasted from what the operator - /// published; pinned before the registration connects. - pub key: String, - /// True when onboarding just wrote the key file, so Back is offered. - pub fresh: bool, + /// True once this host's key is known to be pinned — either a fetch + /// was just confirmed (see `Carrier::pin_offer`) or it already was + /// from an earlier session — so the server step's button reads + /// "Continue" instead of "Fetch the server's key". + pub key_trusted: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignupStep { + Server, + Account, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignupMode { + New, + Existing, } /// The compose view: `to` takes a short address or a smol:// URI. @@ -107,6 +121,8 @@ pub enum Dialog { }, /// Rotation only moves the index forward, so the action asks first. Rotate, + /// Factory reset erases every local file, so it asks first too. + Reset, /// The message being answered carries the sender's own frontmatter /// fields; the reply asks whether to carry copies of them. Reply { @@ -133,9 +149,13 @@ pub enum Message { SignupUserChanged(String), SignupHostChanged(String), SignupInviteChanged(String), - SignupKeyChanged(String), - SignupBack, - SignupLearnKey, + /// The server step's one button: fetches the host's key, or — once + /// one is on screen (fetched this session or already pinned) — pins + /// it and advances. + SignupKeyAction, + SignupStepBack, + SignupStartOver, + SignupModeChanged(SignupMode), SignupRegister, SignupRecall, @@ -186,6 +206,7 @@ pub enum Message { // Settings SettingsLearnKey, + KeepMailChanged(bool), TimeoutChanged(String), RotatePressed, RotateConfirmed, @@ -194,6 +215,9 @@ pub enum Message { ExportPressed, ImportBodyChanged(String), ImportPressed, + ResetPressed, + ResetConfirmed, + ResetCancelled, } /// What a background operation reports back. @@ -219,8 +243,6 @@ pub enum PendingOp { Register { addr: Address, invite: Option, - /// A pasted operator key, pinned before the registration connects. - key: String, }, Restore { addr: Address, @@ -279,6 +301,9 @@ pub struct Carrier { pub status_error: bool, pub busy: Option, pub cancel: Arc, + /// Off by default: a fetch acknowledges what it takes, so mail is + /// removed from the server once it's stored here. + pub keep_mail: bool, pub timeout: u64, pub timeout_text: String, pub onboard: Onboard, @@ -304,6 +329,11 @@ pub struct Carrier { /// The view syncs entry text from the model once after a prefill /// (reply, write-to-contact), then leaves typing to the widgets. pub compose_fresh: Cell, + /// Same pattern for the onboarding/signup entries: a programmatic + /// reset (factory reset, discarding a cancelled identity) clears the + /// model, and this flag makes the view push that blank state into + /// the entries once, instead of leaving stale typed text on screen. + pub form_fresh: Cell, } impl Carrier { @@ -334,6 +364,7 @@ impl Carrier { status_error: false, busy: None, cancel: Arc::new(AtomicBool::new(false)), + keep_mail: false, timeout: 30, timeout_text: "30".to_string(), onboard: Onboard { @@ -342,11 +373,12 @@ impl Carrier { seed_shown: String::new(), }, signup: Signup { + step: SignupStep::Server, + mode: SignupMode::New, user: String::new(), host: String::new(), invite: String::new(), - key: String::new(), - fresh: false, + key_trusted: false, }, folder: Folder::Inbox, lists: vec![Vec::new(), Vec::new(), Vec::new()], @@ -362,6 +394,7 @@ impl Carrier { contact_import: String::new(), dialog: None, compose_fresh: Cell::new(false), + form_fresh: Cell::new(true), }; let op = registered.then(|| PendingOp::Snapshot { read: app.marks.snapshot(), @@ -427,6 +460,61 @@ impl Carrier { self.compose_fresh.set(true); } + /// Factory reset: close the store, erase the config directory, then + /// boot a fresh model in place. The connection is swapped out before + /// the files go because Windows refuses to remove a file a live + /// connection holds, and the busy guard has already seen to it that + /// no operation holds a clone of the store. + fn factory_reset(&mut self) { + let store = match Store::open_in_memory() { + Ok(store) => Arc::new(store), + Err(e) => { + self.fail(e.to_string()); + return; + } + }; + self.store = store; + if let Err(e) = ops::factory_reset(&self.dir) { + self.fail(e); + return; + } + let (app, _) = Carrier::boot(self.dir.clone()); + *self = app; + self.note("factory reset done. Create a new identity or restore a seed.".into()); + } + + /// Cancelling out of a not-yet-registered identity removes the key + /// file too, so Back always leaves onboarding resumable from a clean + /// start — never behind a stale key a later Create could silently + /// overwrite and orphan the server registration against. The seed + /// already shown (or pasted) still regenerates the same master, so + /// nothing is actually lost. + fn discard_identity(&mut self) { + if self.store.account().ok().flatten().is_none() { + if let Err(e) = ops::delete_key(&self.key_path()) { + self.fail(e); + return; + } + self.master = None; + } + self.onboard = Onboard { + mode: OnboardMode::Start, + seed: String::new(), + seed_shown: String::new(), + }; + self.signup = Signup { + step: SignupStep::Server, + mode: SignupMode::New, + user: String::new(), + host: String::new(), + invite: String::new(), + key_trusted: false, + }; + self.pin_offer = None; + self.page = Page::Onboard; + self.form_fresh.set(true); + } + /// The current folder's rows, drafts included: drafts carry the /// recipient where mail carries the sender, newest first. pub fn rows(&self) -> Vec { @@ -504,6 +592,17 @@ impl Carrier { Address::parse(&format!("{user}@{host}")).ok() } + /// The host as typed on the signup page's server step, before a + /// username exists — `learn_key`/`connect` only read host/port/scheme, + /// so a throwaway username satisfies `Address::parse`'s grammar. + fn signup_host_address(&self) -> Option
{ + let host = self.signup.host.trim(); + if host.is_empty() { + return None; + } + Address::parse(&format!("x@{host}")).ok() + } + /// Opens one message and marks it read inline: opening an envelope and /// re-listing is local and fast, so it needs no background pass. fn select_message(&mut self, folder: Folder, id: String) { @@ -596,16 +695,20 @@ impl Carrier { } Message::SignupHostChanged(host) => { self.signup.host = host; + // A different host invalidates whatever key was on + // screen: there is only ever one key pending trust. + self.signup.key_trusted = false; + self.pin_offer = None; } Message::SignupInviteChanged(invite) => { self.signup.invite = invite; } - Message::SignupKeyChanged(key) => { - self.signup.key = key; - } Message::ContactImportChanged(text) => { self.contact_import = text; } + Message::KeepMailChanged(keep) => { + self.keep_mail = keep; + } Message::TimeoutChanged(text) => { if let Ok(timeout) = text.trim().parse::() { self.timeout = timeout; @@ -640,7 +743,13 @@ impl Carrier { self.onboard.mode = OnboardMode::Restore; } Message::OnboardBack => { - self.onboard.mode = OnboardMode::Start; + if self.onboard.mode == OnboardMode::Created { + self.discard_identity(); + } else { + self.onboard.mode = OnboardMode::Start; + self.onboard.seed.clear(); + self.form_fresh.set(true); + } } Message::OnboardRestoreFromSeed => match ops::parse_seed(&self.onboard.seed) { Ok(master) => { @@ -648,7 +757,6 @@ impl Carrier { self.fail(e); } else { self.master = Some(master); - self.signup.fresh = true; self.page = Page::Signup; self.note("Seed restored. Bind a username or recall an address.".into()); } @@ -656,15 +764,28 @@ impl Carrier { Err(e) => self.fail(e), }, Message::OnboardSeedSaved => { - self.signup.fresh = true; self.page = Page::Signup; } - Message::SignupBack => { - self.page = Page::Onboard; + Message::SignupStepBack => { + self.signup.step = SignupStep::Server; } - Message::SignupLearnKey => { - let Some(addr) = self.signup_address() else { - self.fail("enter user and host first".into()); + Message::SignupStartOver => { + self.discard_identity(); + } + Message::SignupModeChanged(mode) => { + self.signup.mode = mode; + } + Message::SignupKeyAction => { + if let Some((addr, key)) = self.pin_offer.take() { + self.signup.step = SignupStep::Account; + return vec![PendingOp::Pin { addr, key }]; + } + if self.signup.key_trusted { + self.signup.step = SignupStep::Account; + return Vec::new(); + } + let Some(addr) = self.signup_host_address() else { + self.fail("enter the server host first".into()); return Vec::new(); }; return vec![PendingOp::LearnKey { addr }]; @@ -694,14 +815,19 @@ impl Carrier { } else { Some(self.signup.invite.clone()) }; - let key = self.signup.key.trim().to_string(); - return vec![PendingOp::Register { addr, invite, key }]; + if !self.start("registering") { + return Vec::new(); + } + return vec![PendingOp::Register { addr, invite }]; } Message::SignupRecall => { let Some(addr) = self.signup_address() else { self.fail("enter user and host".into()); return Vec::new(); }; + if !self.start("restoring") { + return Vec::new(); + } return vec![PendingOp::Restore { addr }]; } Message::GotoMail(folder) => { @@ -989,6 +1115,16 @@ impl Carrier { Message::RotateCancelled => { self.dialog = None; } + Message::ResetPressed => { + self.dialog = Some(Dialog::Reset); + } + Message::ResetCancelled => { + self.dialog = None; + } + Message::ResetConfirmed => { + self.dialog = None; + self.factory_reset(); + } Message::RotateConfirmed => { self.dialog = None; return vec![PendingOp::Rotate]; @@ -1030,6 +1166,13 @@ impl Carrier { OpResult::Generic(Ok(note)) => { self.done(); self.note(note); + // The only `Generic` ops reachable from the signup page are + // Register and Restore — LearnKey/Pin report through their + // own variants — so a success here always means the + // address is bound and it's safe to leave the page. + if self.page == Page::Signup { + self.page = Page::Mail; + } self.refresh() } OpResult::Generic(Err(e)) => { @@ -1049,6 +1192,7 @@ impl Carrier { } OpResult::PinOffered(Ok(None)) => { self.done(); + self.signup.key_trusted = true; self.note("This server's key is already pinned.".into()); None } @@ -1124,6 +1268,194 @@ mod tests { assert_eq!(app.busy.as_deref(), Some("fetching")); } + /// Off by default — a fetch deletes what it takes — and the Settings + /// switch is a plain toggle, not busy-guarded like a network action. + #[test] + fn keep_mail_defaults_off_and_toggles() { + let mut app = app(); + assert!(!app.keep_mail); + app.handle(Message::KeepMailChanged(true)); + assert!(app.keep_mail); + app.handle(Message::KeepMailChanged(false)); + assert!(!app.keep_mail); + } + + /// A factory reset asks first, then erases every file in the config + /// directory and returns the model to onboarding, as if the app had + /// never run. + #[test] + fn factory_reset_erases_the_files_and_returns_to_onboarding() { + let dir = tempfile::tempdir().unwrap().keep(); + let (mut app, _) = Carrier::boot(dir.clone()); + let master = ops::new_master(); + ops::save_key(&dir.join("carrier.key"), &master).unwrap(); + app.master = Some(master); + std::fs::write(dir.join("read.txt"), "ab").unwrap(); + std::fs::write(dir.join("drafts.txt"), "draft2 id 1 -\n\n\n\n").unwrap(); + app.signup.host = "typed-before-the-reset.example".into(); + app.signup.user = "alice".into(); + + // The button is busy-guarded like every other action. + app.busy = Some("fetching".into()); + app.handle(Message::ResetPressed); + assert!(app.dialog.is_none()); + + app.busy = None; + app.handle(Message::ResetPressed); + assert!(matches!(app.dialog, Some(Dialog::Reset))); + + app.handle(Message::ResetConfirmed); + assert!(app.dialog.is_none()); + assert_eq!(app.page, Page::Onboard); + assert_eq!(app.onboard.mode, OnboardMode::Start); + assert!(app.master.is_none()); + assert!(app.snapshot.is_none()); + assert!(app.drafts.is_empty()); + // The fields themselves, not just the model, must come back + // clean — form_fresh tells the view to push the blank state + // into the entries instead of leaving stale typed text on them. + assert!(app.signup.host.is_empty()); + assert!(app.signup.user.is_empty()); + assert!(app.form_fresh.get()); + for name in ["carrier.key", "read.txt", "drafts.txt"] { + assert!(!dir.join(name).exists(), "{name} survived the reset"); + } + // The store is reopened by boot, so a fresh empty db is the one + // file a reset legitimately leaves behind. + assert!(dir.join("carrier.db").exists()); + assert!(app.store.account().unwrap().is_none()); + } + + /// Backing out of a freshly created, not-yet-registered identity must + /// discard it — otherwise a later Create can silently overwrite it out + /// from under an in-flight or already-committed registration. + #[test] + fn onboard_back_from_created_discards_the_key() { + let dir = tempfile::tempdir().unwrap().keep(); + let (mut app, _) = Carrier::boot(dir.clone()); + app.handle(Message::OnboardCreate); + assert!(app.master.is_some()); + assert!(dir.join("carrier.key").exists()); + + app.handle(Message::OnboardBack); + assert!(app.master.is_none()); + assert!(!dir.join("carrier.key").exists()); + assert_eq!(app.page, Page::Onboard); + assert_eq!(app.onboard.mode, OnboardMode::Start); + } + + /// Restore mode writes no key until the seed is actually submitted, so + /// backing out of it has nothing to discard — just the typed seed. + #[test] + fn onboard_back_from_restore_only_clears_the_typed_seed() { + let mut app = app(); + app.handle(Message::OnboardRestore); + app.handle(Message::OnboardSeedChanged("not yet submitted".into())); + app.handle(Message::OnboardBack); + assert!(app.master.is_none()); + assert!(app.onboard.seed.is_empty()); + assert_eq!(app.onboard.mode, OnboardMode::Start); + } + + /// Start Over on the signup page's server step is the same discard, + /// reachable after onboarding has already handed off to Page::Signup. + #[test] + fn signup_start_over_discards_the_key() { + let dir = tempfile::tempdir().unwrap().keep(); + let (mut app, _) = Carrier::boot(dir.clone()); + app.handle(Message::OnboardCreate); + app.handle(Message::OnboardSeedSaved); + assert_eq!(app.page, Page::Signup); + app.signup.host = "example.org".into(); + + app.handle(Message::SignupStartOver); + assert!(app.master.is_none()); + assert!(!dir.join("carrier.key").exists()); + assert_eq!(app.page, Page::Onboard); + assert!(app.signup.host.is_empty()); + } + + /// Register and Restore are long-running, so Back (and a second click) + /// must be locked out for their whole in-flight window, the same way + /// FetchPressed already locks out everything else. + #[test] + fn signup_register_and_recall_engage_the_busy_guard() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.page = Page::Signup; + app.signup.user = "alice".into(); + app.signup.host = "example.org".into(); + + let ops = app.handle(Message::SignupRegister); + assert_eq!(app.busy.as_deref(), Some("registering")); + assert!(matches!(ops.as_slice(), [PendingOp::Register { .. }])); + assert!(app.handle(Message::SignupStartOver).is_empty()); + assert_eq!(app.page, Page::Signup); + + app.busy = None; + let ops = app.handle(Message::SignupRecall); + assert_eq!(app.busy.as_deref(), Some("restoring")); + assert!(matches!(ops.as_slice(), [PendingOp::Restore { .. }])); + } + + /// A successful Register/Restore must leave the signup page, or a + /// lingering Back there could still race a fresh identity into + /// existence against an address the store already considers bound. + #[test] + fn generic_success_on_the_signup_page_moves_on_to_mail() { + let mut app = app(); + app.page = Page::Signup; + app.busy = Some("registering".into()); + app.handle_result(OpResult::Generic(Ok("registered".into()))); + assert_eq!(app.page, Page::Mail); + } + + /// The server step's one button fetches a key when none is on + /// screen yet, then pins it and advances once one is. + #[test] + fn signup_key_action_fetches_then_pins_and_advances() { + let mut app = app(); + app.signup.host = "example.org".into(); + + let ops = app.handle(Message::SignupKeyAction); + assert!(matches!(ops.as_slice(), [PendingOp::LearnKey { .. }])); + assert_eq!(app.signup.step, SignupStep::Server); + + let addr = Address::parse("x@example.org").unwrap(); + app.pin_offer = Some((addr, [0u8; KEY_LEN])); + let ops = app.handle(Message::SignupKeyAction); + assert!(matches!(ops.as_slice(), [PendingOp::Pin { .. }])); + assert!(app.pin_offer.is_none()); + assert_eq!(app.signup.step, SignupStep::Account); + } + + /// An already-trusted host (pinned in an earlier session) needs no + /// key to show — Continue just advances with no background op. + #[test] + fn signup_key_action_advances_without_a_pin_when_already_trusted() { + let mut app = app(); + app.signup.host = "example.org".into(); + app.signup.key_trusted = true; + + let ops = app.handle(Message::SignupKeyAction); + assert!(ops.is_empty()); + assert_eq!(app.signup.step, SignupStep::Account); + } + + /// Editing the host after a key was fetched (or already trusted) + /// invalidates it: there is only ever one key pending trust. + #[test] + fn editing_the_host_resets_a_fetched_or_trusted_key() { + let mut app = app(); + let addr = Address::parse("x@example.org").unwrap(); + app.pin_offer = Some((addr, [0u8; KEY_LEN])); + app.signup.key_trusted = true; + + app.handle(Message::SignupHostChanged("example.net".into())); + assert!(app.pin_offer.is_none()); + assert!(!app.signup.key_trusted); + } + fn opened() -> OpenedMsg { OpenedMsg { id: "ab".repeat(32), diff --git a/src/ops.rs b/src/ops.rs index 5cf1d66..151f73e 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -292,6 +292,33 @@ pub fn save_key(path: &Path, master: &[u8; KEY_LEN]) -> Result<(), String> { err } +/// Removes a not-yet-registered master key when onboarding is cancelled. +/// Idempotent, like `factory_reset`: a missing file is fine. +pub fn delete_key(path: &Path) -> Result<(), String> { + match std::fs::remove_file(path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(format!("{}: {e}", path.display())), + } +} + +/// Removes every file carrier owns: the store, the key and both sidecars. +/// A missing file is fine — reset is idempotent — but a file that cannot +/// be removed is reported, so a reset that did not take is never silent. +/// The caller must close the store first: Windows refuses to remove a +/// file a live connection holds. +pub fn factory_reset(dir: &Path) -> Result<(), String> { + for name in ["carrier.db", "carrier.key", "read.txt", "drafts.txt"] { + let path = dir.join(name); + if let Err(e) = std::fs::remove_file(&path) { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(format!("{}: {e}", path.display())); + } + } + } + Ok(()) +} + pub fn account(store: &Store, master: &[u8; KEY_LEN]) -> Result { let index = store.rotations().map_err(explain)?; account::Account::new(*master, index).map_err(explain) @@ -301,14 +328,6 @@ pub fn parse_address(text: &str) -> Result { Address::parse(text).map_err(explain) } -/// A base32 public key, the form the operator publishes and pins are shown in. -pub fn parse_key(text: &str) -> Result<[u8; KEY_LEN], String> { - unb32(text.trim()) - .map_err(explain)? - .try_into() - .map_err(|_| "keys are 32 bytes of base32".to_string()) -} - pub fn register( store: &Store, master: &[u8; KEY_LEN], @@ -378,11 +397,12 @@ pub fn fetch( store: &Store, master: &[u8; KEY_LEN], cancel: Arc, + keep: bool, timeout: u64, ) -> Result { let me = account(store, master)?; let opts = FetchOptions { - keep: false, + keep, reset: false, timeout, dial: None, @@ -913,13 +933,6 @@ mod tests { assert!(parse_seed(&hex[..63]).is_err()); } - #[test] - fn key_parsing_round_trips() { - let m = master(); - assert_eq!(parse_key(&b32(&m)).unwrap(), m); - assert!(parse_key("short").is_err()); - } - /// A sealed self-copy in the store surfaces as a row, opens with its /// frontmatter split, and flips to read against the marks set. #[test] diff --git a/src/view.rs b/src/view.rs index 90f059a..0725c25 100644 --- a/src/view.rs +++ b/src/view.rs @@ -19,7 +19,9 @@ use relm4::adw::prelude::*; use relm4::ComponentSender; use relm4::{Component, ComponentParts}; -use crate::app::{Carrier, Dialog, Message, OnboardMode, OpResult, Page, PendingOp}; +use crate::app::{ + Carrier, Dialog, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, SignupStep, +}; use crate::ops::{self, Contact, Folder, Row}; use relm4::RelmApp; @@ -193,6 +195,7 @@ pub struct CarrierWidgets { mail_list: gtk::ListBox, list_empty: adw::StatusPage, list_empty_fetch: gtk::Button, + list_empty_hint: gtk::Label, article_stack: gtk::Stack, article_subject: gtk::Label, article_meta: gtk::Label, @@ -223,16 +226,29 @@ pub struct CarrierWidgets { offer_row: adw::ActionRow, pins_group: adw::PreferencesGroup, pin_rows: Vec, + keep_mail_row: adw::SwitchRow, timeout_row: adw::SpinRow, backup_label: gtk::Label, // onboarding onboard_seed_label: gtk::Label, + onboard_seed_entry: adw::EntryRow, // signup - signup_back: gtk::Button, - signup_offer_list: gtk::ListBox, - signup_offer_row: adw::ActionRow, + signup_key_button: gtk::Button, + signup_key_card: gtk::Box, + signup_key_value: gtk::Label, + signup_key_fingerprint: gtk::Label, + signup_key_hint: gtk::Label, + signup_start_over: gtk::Button, + signup_step_back: gtk::Button, + signup_mode_new: gtk::ToggleButton, + signup_mode_existing: gtk::ToggleButton, + signup_host: adw::EntryRow, + signup_user: adw::EntryRow, + signup_invite: adw::EntryRow, + register_button: gtk::Button, + recall_button: gtk::Button, // status bar and dialogs spinner: gtk::Spinner, @@ -414,7 +430,18 @@ impl CarrierWidgets { let list_empty = adw::StatusPage::new(); let list_empty_fetch = icon_label_button("mail-send-receive-symbolic", "Fetch", "pill"); list_empty_fetch.set_halign(Align::Center); - list_empty.set_child(Some(&list_empty_fetch)); + let list_empty_hint = label( + "Fetching removes mail from the server once it's stored here.\n\ + Turn on \"Keep mail on the server\" in Settings to keep it there instead.", + "dim caption", + ); + list_empty_hint.set_wrap(true); + list_empty_hint.set_justify(gtk::Justification::Center); + list_empty_hint.set_halign(Align::Center); + let list_empty_box = gtk::Box::new(Orientation::Vertical, 8); + list_empty_box.append(&list_empty_fetch); + list_empty_box.append(&list_empty_hint); + list_empty.set_child(Some(&list_empty_box)); let list_stack = gtk::Stack::new(); list_stack.set_hhomogeneous(false); list_stack.set_vhomogeneous(false); @@ -625,6 +652,14 @@ impl CarrierWidgets { .build(); settings.add(&pins_group); + let fetch_group = adw::PreferencesGroup::builder().title("Fetching").build(); + let keep_mail_row = adw::SwitchRow::builder() + .title("Keep mail on the server") + .subtitle("Off: a fetch removes mail from the server once it's stored here.") + .build(); + fetch_group.add(&keep_mail_row); + settings.add(&fetch_group); + let network_group = adw::PreferencesGroup::builder().title("Network").build(); let timeout_row = adw::SpinRow::with_range(1.0, 600.0, 1.0); timeout_row.set_title("Timeout"); @@ -678,6 +713,19 @@ impl CarrierWidgets { import_group.add(&import_backup); settings.add(&import_group); + // The reset closes the page list: nothing after it survives a + // wipe, so it sits last, marked destructive like the delete. + let reset_group = adw::PreferencesGroup::builder() + .title("Factory reset") + .description( + "Erase the seed key, the sealed store and every sidecar on this device, and return to onboarding. Mail already on the server is not touched.", + ) + .build(); + let reset_button = icon_label_button("user-trash-symbolic", "Factory reset", "danger"); + reset_button.set_valign(Align::Center); + reset_group.set_header_suffix(Some(&reset_button)); + settings.add(&reset_group); + stack.add_named(&settings, Some("settings")); // onboarding: three static modes, each a centred column @@ -700,8 +748,8 @@ impl CarrierWidgets { tagline.set_halign(Align::Center); tagline.set_margin_bottom(12); onboard_start.append(&tagline); - let create_button = button("Create an identity", "pill"); - let restore_button = button("Restore from a seed", "pill-quiet"); + let create_button = button("Create a new identity", "pill"); + let restore_button = button("Restore existing identity from seed", "pill-quiet"); onboard_start.append(&create_button); onboard_start.append(&restore_button); stack.add_named(¢er(&onboard_start), Some("onboard")); @@ -746,46 +794,84 @@ impl CarrierWidgets { onboard_restore.append(&restore_back); stack.add_named(¢er(&onboard_restore), Some("onboard-restore")); - // signup - let signup = gtk::Box::new(Orientation::Vertical, 12); - signup.append(&label("Set up the address", "subtitle")); - let signup_group = adw::PreferencesGroup::builder() - .description( - "The invite token and server key are only needed when the operator published them.", - ) - .build(); - let signup_user = adw::EntryRow::builder().title("User").build(); + // signup: step 1 — the server this address lives on + let signup_server = gtk::Box::new(Orientation::Vertical, 12); + signup_server.append(&label("Set up the server", "subtitle")); + let signup_server_group = adw::PreferencesGroup::new(); let signup_host = adw::EntryRow::builder().title("Host").build(); - let signup_invite = adw::EntryRow::builder().title("Invite token").build(); - let signup_key = adw::EntryRow::builder().title("Server key").build(); - for row in [&signup_user, &signup_host, &signup_invite, &signup_key] { - signup_group.add(row); - } - signup.append(&signup_group); - let signup_learn = button("Learn the server key and pin what it presents", ""); - signup.append(&signup_learn); - let signup_offer_list = boxed_list(); - signup_offer_list.set_visible(false); - let signup_offer_row = adw::ActionRow::builder() - .title("Server key offered") + signup_server_group.add(&signup_host); + signup_server.append(&signup_server_group); + + // Shown once a key has been fetched: the key and its fingerprint, + // prominent, plus the reminder to check it against what the + // operator published. Hidden otherwise. + let signup_key_value = label("", "mono"); + signup_key_value.set_selectable(true); + signup_key_value.set_wrap(true); + signup_key_value.set_margin_top(12); + signup_key_value.set_margin_start(12); + signup_key_value.set_margin_end(12); + let signup_key_fingerprint = label("", "mono dim"); + signup_key_fingerprint.set_selectable(true); + signup_key_fingerprint.set_wrap(true); + signup_key_fingerprint.set_margin_bottom(12); + signup_key_fingerprint.set_margin_start(12); + signup_key_fingerprint.set_margin_end(12); + let signup_key_card = gtk::Box::new(Orientation::Vertical, 4); + signup_key_card.add_css_class("card"); + signup_key_card.set_visible(false); + signup_key_card.append(&signup_key_value); + signup_key_card.append(&signup_key_fingerprint); + signup_server.append(&signup_key_card); + let signup_key_hint = label( + "If the server operator published a key or fingerprint, make \ + sure it matches before trusting it.", + "dim", + ); + signup_key_hint.set_wrap(true); + signup_key_hint.set_visible(false); + signup_server.append(&signup_key_hint); + + let signup_key_button = button("Fetch the server's key", "pill"); + let signup_start_over = button("Start over", "pill-quiet"); + signup_server.append(&signup_key_button); + signup_server.append(&signup_start_over); + stack.add_named(¢er(&signup_server), Some("signup-server")); + + // signup: step 2 — bind a username, or recall an existing address + let signup_account = gtk::Box::new(Orientation::Vertical, 12); + signup_account.append(&label("Choose your address", "subtitle")); + let signup_mode_new = gtk::ToggleButton::builder() + .label("Create a new address") + .active(true) + .hexpand(true) .build(); - signup_offer_row.set_subtitle_lines(2); - let signup_offer_pin = button("Pin", "accent"); - signup_offer_pin.set_valign(Align::Center); - let signup_offer_discard = button("Discard", ""); - signup_offer_discard.set_valign(Align::Center); - signup_offer_row.add_suffix(&signup_offer_discard); - signup_offer_row.add_suffix(&signup_offer_pin); - signup_offer_list.append(&signup_offer_row); - signup.append(&signup_offer_list); + let signup_mode_existing = gtk::ToggleButton::builder() + .label("I already have this address") + .hexpand(true) + .build(); + signup_mode_existing.set_group(Some(&signup_mode_new)); + let signup_mode_box = gtk::Box::new(Orientation::Horizontal, 0); + signup_mode_box.add_css_class("linked"); + signup_mode_box.set_homogeneous(true); + signup_mode_box.append(&signup_mode_new); + signup_mode_box.append(&signup_mode_existing); + signup_account.append(&signup_mode_box); + let signup_account_group = adw::PreferencesGroup::new(); + let signup_user = adw::EntryRow::builder().title("User").build(); + let signup_invite = adw::EntryRow::builder() + .title("Invite token (if required by the server)") + .build(); + signup_account_group.add(&signup_user); + signup_account_group.add(&signup_invite); + signup_account.append(&signup_account_group); let register_button = button("Register", "pill"); - let recall_button = button("I already have this address", "pill-quiet"); - signup.append(®ister_button); - signup.append(&recall_button); - let signup_back = button("Back", "quiet"); - signup_back.set_visible(false); - signup.append(&signup_back); - stack.add_named(¢er(&signup), Some("signup")); + let recall_button = button("Restore access", "pill"); + signup_account.append(®ister_button); + signup_account.append(&recall_button); + let signup_step_back = button("Back", "quiet"); + signup_account.append(&signup_step_back); + stack.add_named(¢er(&signup_account), Some("signup-account")); // --- status bar ------------------------------------------------- // One fixed line at the bottom: the spinner and the running @@ -939,17 +1025,20 @@ impl CarrierWidgets { }); rotate_button.connect_clicked(input(Message::RotatePressed)); + reset_button.connect_clicked(input(Message::ResetPressed)); learn_button.connect_clicked(input(Message::SettingsLearnKey)); offer_pin.connect_clicked(input(Message::PinAccept)); - signup_offer_pin.connect_clicked(input(Message::PinAccept)); offer_discard.connect_clicked(input(Message::PinDiscard)); - signup_offer_discard.connect_clicked(input(Message::PinDiscard)); export_button.connect_clicked(input(Message::ExportPressed)); import_backup.connect_clicked(input(Message::ImportPressed)); let timeout_sender = sender.clone(); timeout_row.connect_value_notify(move |row| { timeout_sender.input(Message::TimeoutChanged((row.value() as u64).to_string())) }); + let keep_mail_sender = sender.clone(); + keep_mail_row.connect_active_notify(move |row| { + keep_mail_sender.input(Message::KeepMailChanged(row.is_active())) + }); let import_body_sender = sender.clone(); import_body.connect_changed(move |buffer| { import_body_sender.input(Message::ImportBodyChanged(buffer_text(buffer))) @@ -982,14 +1071,23 @@ impl CarrierWidgets { signup_invite.connect_changed(move |entry| { invite_sender.input(Message::SignupInviteChanged(entry.text().to_string())) }); - let key_sender = sender.clone(); - signup_key.connect_changed(move |entry| { - key_sender.input(Message::SignupKeyChanged(entry.text().to_string())) - }); - signup_learn.connect_clicked(input(Message::SignupLearnKey)); + signup_key_button.connect_clicked(input(Message::SignupKeyAction)); + signup_start_over.connect_clicked(input(Message::SignupStartOver)); + signup_step_back.connect_clicked(input(Message::SignupStepBack)); register_button.connect_clicked(input(Message::SignupRegister)); recall_button.connect_clicked(input(Message::SignupRecall)); - signup_back.connect_clicked(input(Message::SignupBack)); + let mode_new_sender = sender.clone(); + signup_mode_new.connect_toggled(move |btn| { + if btn.is_active() { + mode_new_sender.input(Message::SignupModeChanged(SignupMode::New)); + } + }); + let mode_existing_sender = sender.clone(); + signup_mode_existing.connect_toggled(move |btn| { + if btn.is_active() { + mode_existing_sender.input(Message::SignupModeChanged(SignupMode::Existing)); + } + }); CarrierWidgets { window: root.clone(), @@ -1005,6 +1103,7 @@ impl CarrierWidgets { mail_list, list_empty, list_empty_fetch, + list_empty_hint, article_stack, article_subject, article_meta, @@ -1027,12 +1126,25 @@ impl CarrierWidgets { offer_row, pins_group, pin_rows: Vec::new(), + keep_mail_row, timeout_row, backup_label, onboard_seed_label, - signup_back, - signup_offer_list, - signup_offer_row, + onboard_seed_entry, + signup_key_button, + signup_key_card, + signup_key_value, + signup_key_fingerprint, + signup_key_hint, + signup_start_over, + signup_step_back, + signup_mode_new, + signup_mode_existing, + signup_host, + signup_user, + signup_invite, + register_button, + recall_button, spinner, status_label, cancel_button, @@ -1062,10 +1174,15 @@ impl Component for Carrier { sender: ComponentSender, ) -> ComponentParts { let (model, op) = Carrier::boot(init); - let widgets = CarrierWidgets::new(&root, &sender); + let mut widgets = CarrierWidgets::new(&root, &sender); if let Some(op) = op { spawn_op(&model, op, &sender); } + // `update_view` runs only after a message, and a clean install + // boots with none queued, so the first paint happens here — + // otherwise the stack sits on its default page until the first + // click flips it to the page the model opened with. + render(&model, &mut widgets, &sender); ComponentParts { model, widgets } } @@ -1098,21 +1215,15 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { let store = model.store.clone(); let master = model.master.unwrap_or([0u8; KEY_LEN]); let timeout = model.timeout; + let keep_mail = model.keep_mail; match op { PendingOp::Snapshot { read } => sender.spawn_command(move |out| { let _ = out.send(OpResult::SnapshotLoaded(ops::snapshot( &store, &master, &read, ))); }), - PendingOp::Register { addr, invite, key } => sender.spawn_command(move |out| { - let res = if key.is_empty() { - ops::register(&store, &master, &addr, invite.as_deref(), timeout) - } else { - ops::parse_key(&key).and_then(|key| { - ops::pin(&store, &addr, &key)?; - ops::register(&store, &master, &addr, invite.as_deref(), timeout) - }) - }; + PendingOp::Register { addr, invite } => sender.spawn_command(move |out| { + let res = ops::register(&store, &master, &addr, invite.as_deref(), timeout); let _ = out.send(OpResult::Generic(res.map(|_| "registered".to_string()))); }), PendingOp::Restore { addr } => sender.spawn_command(move |out| { @@ -1142,7 +1253,7 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { }); sender.spawn_command(move |out| { let _ = out.send(OpResult::Generic(ops::fetch( - &store, &master, cancel, timeout, + &store, &master, cancel, keep_mail, timeout, ))); }); } @@ -1321,7 +1432,10 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender "onboard-created", OnboardMode::Restore => "onboard-restore", }, - Page::Signup => "signup", + Page::Signup => match model.signup.step { + SignupStep::Server => "signup-server", + SignupStep::Account => "signup-account", + }, Page::Mail => "mail", Page::Settings => "settings", } @@ -1332,32 +1446,30 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender ( - "mail-read-symbolic", - "Inbox is empty", - "Fetch to check the mailbox.", - ), + Folder::Inbox => (None, "Inbox is empty", "Fetch to check the mailbox."), Folder::Requests => ( - "contact-new-symbolic", + Some("contact-new-symbolic"), "No requests", "Mail from senders you have not accepted yet arrives here.", ), Folder::Sent => ( - "mail-send-symbolic", + Some("mail-send-symbolic"), "Nothing sent yet", "Messages you send keep a sealed copy here.", ), Folder::Drafts => ( - "document-edit-symbolic", + Some("document-edit-symbolic"), "No drafts", "A compose closed from the sidebar is kept here.", ), }; - w.list_empty.set_icon_name(Some(icon)); + w.list_empty.set_icon_name(icon); w.list_empty.set_title(title); w.list_empty.set_description(Some(description)); - w.list_empty_fetch - .set_visible(model.folder == Folder::Inbox); + let inbox_empty = model.folder == Folder::Inbox; + w.list_empty_fetch.set_visible(inbox_empty); + w.list_empty_hint + .set_visible(inbox_empty && !model.keep_mail); w.list_stack .set_visible_child_name(if rows.is_empty() { "empty" } else { "list" }); let mut selected_row = None; @@ -1594,11 +1706,11 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender Some edge: `render` // runs after every message and an alert dialog is modal, so presenting @@ -1650,6 +1809,7 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let rotating = matches!(dialog, Dialog::Rotate); let replying = matches!(dialog, Dialog::Reply { .. }); + let resetting = matches!(dialog, Dialog::Reset); let alert = match dialog { Dialog::Delete { kept, .. } => { let alert = adw::AlertDialog::new( @@ -1682,6 +1842,16 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { + let alert = adw::AlertDialog::new( + Some("Factory reset this device?"), + Some("The seed key, the sealed store, contacts, server pins, drafts and read marks are erased here. Mail already on the server stays there — without the seed it cannot be read back."), + ); + alert.add_response("cancel", "Cancel"); + alert.add_response("reset", "Factory reset"); + alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive); + alert + } Dialog::Reply { rows, .. } => { let list = rows .iter() @@ -1708,15 +1878,17 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender Message::RotateConfirmed, - (true, _, _) => Message::RotateCancelled, - (_, true, "copy") => Message::ReplyConfirmed(true), - (_, true, "without") => Message::ReplyConfirmed(false), - (_, true, _) => Message::ReplyCancelled, - (false, _, "here") => Message::DeleteConfirmed(false), - (false, _, "everywhere") => Message::DeleteConfirmed(true), - (false, _, _) => Message::DeleteCancelled, + response_sender.input(match (rotating, replying, resetting, response) { + (true, _, _, "rotate") => Message::RotateConfirmed, + (true, _, _, _) => Message::RotateCancelled, + (_, true, _, "copy") => Message::ReplyConfirmed(true), + (_, true, _, "without") => Message::ReplyConfirmed(false), + (_, true, _, _) => Message::ReplyCancelled, + (_, _, true, "reset") => Message::ResetConfirmed, + (_, _, true, _) => Message::ResetCancelled, + (false, _, _, "here") => Message::DeleteConfirmed(false), + (false, _, _, "everywhere") => Message::DeleteConfirmed(true), + (false, _, _, _) => Message::DeleteCancelled, }); }); alert.present(Some(&w.window)); From 743c388cfbc88ef66100be04c10b80793a3b2923 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 20:46:17 +0300 Subject: [PATCH 24/37] feat: split settings into Identity, Server, Backup and Advanced tabs --- Cargo.toml | 3 +- src/app.rs | 119 ++++++-------- src/ops.rs | 65 ++++---- src/view.rs | 466 ++++++++++++++++++++++++++++++++-------------------- 4 files changed, 365 insertions(+), 288 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c81e242..19f960e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,8 @@ license = "Apache-2.0" [dependencies] fumi-core = { path = "../fumi/core" } -gtk = { version = "0.9", package = "gtk4" } +# v4_10 unlocks gtk::FileDialog, used for the backup export/import pickers. +gtk = { version = "0.9", package = "gtk4", features = ["v4_10"] } # Relm4 pulls libadwaita in; the direct entry only raises the API level, # since Cargo unions features and the rows and dialogs used here are 1.5. libadwaita = { version = "0.7", features = ["v1_5"] } diff --git a/src/app.rs b/src/app.rs index 3b9c479..5824a81 100644 --- a/src/app.rs +++ b/src/app.rs @@ -159,10 +159,6 @@ pub enum Message { SignupRegister, SignupRecall, - // Pin offer (signup and settings share it) - PinAccept, - PinDiscard, - // Mail GotoMail(Folder), Goto(Page), @@ -202,19 +198,17 @@ pub enum Message { ContactAccept(String, String), ContactBlock(String), ContactUnblock(String), - CopyExport, // Settings - SettingsLearnKey, + SecretRevealToggled, + CopySecret, KeepMailChanged(bool), TimeoutChanged(String), RotatePressed, RotateConfirmed, RotateCancelled, - UnpinPressed(String), - ExportPressed, - ImportBodyChanged(String), - ImportPressed, + ExportBackupTo(PathBuf), + ImportBackupFrom(String), ResetPressed, ResetConfirmed, ResetCancelled, @@ -229,8 +223,6 @@ pub enum OpResult { /// The key a server presented with no pin in place (sec 4 tofu). PinOffered(Result, String>), Pinned(Result<(), String>), - /// (note, backup text) — the text is for the clipboard and the view. - Exported(Result<(String, String), String>), } /// One background operation to run on the blocking pool, carrying every @@ -280,11 +272,8 @@ pub enum PendingOp { uri: String, }, Rotate, - Unpin { - label: String, - }, Export { - dir: PathBuf, + path: PathBuf, }, ImportBackup { text: String, @@ -322,8 +311,8 @@ pub struct Carrier { pub drafts: Vec, pub snapshot: Option, pub pin_offer: Option<(Address, [u8; KEY_LEN])>, - pub backup_text: Option, - pub import_body: String, + /// The Identity tab's recovery secret is masked until this is toggled. + pub secret_revealed: bool, pub contact_import: String, pub dialog: Option, /// The view syncs entry text from the model once after a prefill @@ -389,8 +378,7 @@ impl Carrier { drafts, snapshot: None, pin_offer: None, - backup_text: None, - import_body: String::new(), + secret_revealed: false, contact_import: String::new(), dialog: None, compose_fresh: Cell::new(false), @@ -480,7 +468,7 @@ impl Carrier { } let (app, _) = Carrier::boot(self.dir.clone()); *self = app; - self.note("factory reset done. Create a new identity or restore a seed.".into()); + self.note("logged out. Create a new identity or restore a seed.".into()); } /// Cancelling out of a not-yet-registered identity removes the key @@ -577,11 +565,6 @@ impl Carrier { }) } - /// The account address, for the settings page's learn-key action. - fn account_address(&self) -> Option
{ - self.store.account().ok().flatten() - } - /// `user@host` as typed on the signup page, defaulting the port. fn signup_address(&self) -> Option
{ let user = self.signup.user.trim(); @@ -656,9 +639,6 @@ impl Carrier { } } } - Message::ImportBodyChanged(text) => { - self.import_body = text; - } Message::ContactSelected(address) => { self.stash_draft(); self.selected_own = false; @@ -709,6 +689,9 @@ impl Carrier { Message::KeepMailChanged(keep) => { self.keep_mail = keep; } + Message::SecretRevealToggled => { + self.secret_revealed = !self.secret_revealed; + } Message::TimeoutChanged(text) => { if let Ok(timeout) = text.trim().parse::() { self.timeout = timeout; @@ -790,21 +773,6 @@ impl Carrier { }; return vec![PendingOp::LearnKey { addr }]; } - Message::SettingsLearnKey => { - let Some(addr) = self.account_address() else { - return Vec::new(); - }; - return vec![PendingOp::LearnKey { addr }]; - } - Message::PinDiscard => { - self.pin_offer = None; - } - Message::PinAccept => { - let Some((addr, key)) = self.pin_offer.take() else { - return Vec::new(); - }; - return vec![PendingOp::Pin { addr, key }]; - } Message::SignupRegister => { let Some(addr) = self.signup_address() else { self.fail("enter user and host".into()); @@ -1102,11 +1070,10 @@ impl Carrier { }; return vec![PendingOp::Accept { address, identity }]; } - Message::CopyExport => { - if let Some(text) = &self.backup_text { - if let Some(display) = gtk::gdk::Display::default() { - display.clipboard().set_text(text); - } + Message::CopySecret => { + let text = ops::seed_text(&self.master()); + if let Some(display) = gtk::gdk::Display::default() { + display.clipboard().set_text(&text); } } Message::RotatePressed => { @@ -1129,20 +1096,14 @@ impl Carrier { self.dialog = None; return vec![PendingOp::Rotate]; } - Message::UnpinPressed(label) => { - return vec![PendingOp::Unpin { label }]; + Message::ExportBackupTo(path) => { + return vec![PendingOp::Export { path }]; } - Message::ExportPressed => { - let dir = self.dir.clone(); - return vec![PendingOp::Export { dir }]; - } - Message::ImportPressed => { - if self.import_body.trim().is_empty() { + Message::ImportBackupFrom(text) => { + if text.trim().is_empty() { return Vec::new(); } - return vec![PendingOp::ImportBackup { - text: self.import_body.clone(), - }]; + return vec![PendingOp::ImportBackup { text }]; } } Vec::new() @@ -1211,17 +1172,6 @@ impl Carrier { self.fail(e); None } - OpResult::Exported(Ok((note, text))) => { - self.done(); - self.backup_text = Some(text); - self.note(note); - None - } - OpResult::Exported(Err(e)) => { - self.done(); - self.fail(e); - None - } } } } @@ -1280,6 +1230,33 @@ mod tests { assert!(!app.keep_mail); } + #[test] + fn secret_reveal_toggles() { + let mut app = app(); + assert!(!app.secret_revealed); + app.handle(Message::SecretRevealToggled); + assert!(app.secret_revealed); + app.handle(Message::SecretRevealToggled); + assert!(!app.secret_revealed); + } + + /// Export/Import now go through a native file dialog rather than a + /// paste box, so the model only ever sees the chosen path/text — + /// an empty import (e.g. an empty file) is a no-op, like before. + #[test] + fn export_and_import_dispatch_on_the_chosen_path() { + let mut app = app(); + let path = std::path::PathBuf::from("/tmp/wherever.gsmol"); + let ops = app.handle(Message::ExportBackupTo(path.clone())); + assert!(matches!(ops.as_slice(), [PendingOp::Export { path: p }] if *p == path)); + + assert!(app + .handle(Message::ImportBackupFrom(" ".into())) + .is_empty()); + let ops = app.handle(Message::ImportBackupFrom("some backup text".into())); + assert!(matches!(ops.as_slice(), [PendingOp::ImportBackup { .. }])); + } + /// A factory reset asks first, then erases every file in the config /// directory and returns the model to onboarding, as if the app had /// never run. diff --git a/src/ops.rs b/src/ops.rs index 151f73e..916e826 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -3,17 +3,17 @@ //! values these functions return. use std::collections::HashSet; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use fumi_core::address::{Address, DEFAULT_PORT}; +use fumi_core::address::Address; use fumi_core::client::{FetchOptions, TrustChange}; use fumi_core::crypto::{b32, fingerprint, unb32, KEY_LEN}; use fumi_core::error::Error; use fumi_core::message::{FRONTMATTER_KEYS, FRONTMATTER_MAX}; use fumi_core::store::Store; -use fumi_core::{account, client, export, store}; +use fumi_core::{account, client, export}; /// One message in a folder list, already opened for its metadata. #[derive(Debug, Clone)] @@ -57,8 +57,10 @@ pub struct Contact { #[derive(Debug, Clone)] pub struct Who { pub address: String, + pub host: String, pub key: String, pub fingerprint: String, + pub uri: String, pub rotations: u32, } @@ -375,19 +377,6 @@ pub fn pin(store: &Store, addr: &Address, key: &[u8; KEY_LEN]) -> Result<(), Str .map_err(explain) } -/// Splits a stored pin label (`host` or `host:port`) back into its parts. -fn label_parts(label: &str) -> (String, u16) { - match label.rsplit_once(':') { - Some((host, port)) => (host.to_string(), port.parse().unwrap_or(DEFAULT_PORT)), - None => (label.to_string(), DEFAULT_PORT), - } -} - -pub fn unpin(store: &Store, label: &str) -> Result<(), String> { - let (host, port) = label_parts(label); - store.unpin_server(&host, port).map_err(explain) -} - pub fn pins(store: &Store) -> Result, String> { store.pins().map_err(explain) } @@ -621,16 +610,11 @@ pub fn rotate( Ok((store.rotations().map_err(explain)?, b32(&rotated.new_pk))) } -/// Writes the gsmol backup beside the store and returns it for the clipboard. -pub fn export_backup( - store: &Store, - master: &[u8; KEY_LEN], - dir: &Path, -) -> Result<(PathBuf, String), String> { +/// Writes the gsmol backup to the chosen path. +pub fn export_backup(store: &Store, master: &[u8; KEY_LEN], path: &Path) -> Result { let text = export::export(store, master).map_err(explain)?; - let path = dir.join(format!("carrier-backup-{}.gsmol", store::now())); - std::fs::write(&path, &text).map_err(|e| format!("{}: {e}", path.display()))?; - Ok((path, text)) + std::fs::write(path, &text).map_err(|e| format!("{}: {e}", path.display()))?; + Ok(format!("backup written to {}", path.display())) } pub fn import_backup(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result { @@ -733,15 +717,26 @@ pub fn open( pub fn whoami(store: &Store, master: &[u8; KEY_LEN]) -> Result { let me = account(store, master)?; - let address = store - .account() - .map_err(explain)? + let account = store.account().map_err(explain)?; + let pk = me.me().pk(); + let address = account + .as_ref() .map(|addr| addr.short()) .unwrap_or_default(); + let host = account + .as_ref() + .map(|addr| addr.host.clone()) + .unwrap_or_default(); + let uri = account + .as_ref() + .map(|addr| addr.uri(&pk)) + .unwrap_or_default(); Ok(Who { address, - key: b32(&me.me().pk()), - fingerprint: fingerprint(&me.me().pk()), + host, + key: b32(&pk), + fingerprint: fingerprint(&pk), + uri, rotations: me.index(), }) } @@ -1018,8 +1013,11 @@ mod tests { .store_inbox(&id, &envelope, now(), false, false) .unwrap(); - let (path, text) = export_backup(&store, &m, dir.path()).unwrap(); + let path = dir.path().join("backup.gsmol"); + let note = export_backup(&store, &m, &path).unwrap(); assert!(path.exists()); + assert!(note.contains(&path.display().to_string())); + let text = std::fs::read_to_string(&path).unwrap(); assert!(!text.is_empty()); let fresh = setup(); @@ -1032,17 +1030,14 @@ mod tests { } #[test] - fn unpin_splits_the_label() { + fn pin_lists_under_the_host_port_label() { let store = setup(); let addr = Address::parse("me@localhost:1962").unwrap(); let key = master(); - store.pin_server(&addr.host, addr.port, &key).unwrap(); assert_eq!(pin(&store, &addr, &key), Ok(())); let all = pins(&store).unwrap(); assert_eq!(all.len(), 1); assert_eq!(all[0].0, "localhost:1962"); - unpin(&store, "localhost:1962").unwrap(); - assert!(pins(&store).unwrap().is_empty()); } /// The dev-build demo seed grows a mail that carries the sender's own diff --git a/src/view.rs b/src/view.rs index 0725c25..64b0d63 100644 --- a/src/view.rs +++ b/src/view.rs @@ -221,14 +221,14 @@ pub struct CarrierWidgets { detail_qr: gtk::Image, // settings page - identity_values: Vec, - offer_group: adw::PreferencesGroup, - offer_row: adw::ActionRow, - pins_group: adw::PreferencesGroup, - pin_rows: Vec, - keep_mail_row: adw::SwitchRow, + identity_rows: Vec, + secret_row: adw::ActionRow, + secret_reveal: gtk::Button, + host_row: adw::ActionRow, + public_key_row: adw::ActionRow, + rotations_value: gtk::Label, + keep_mail_check: gtk::CheckButton, timeout_row: adw::SpinRow, - backup_label: gtk::Label, // onboarding onboard_seed_label: gtk::Label, @@ -603,128 +603,216 @@ impl CarrierWidgets { compose.append(&compose_actions); stack.add_named(&compose, Some("compose")); - // settings: the preferences page every GNOME app shows, one group - // per concern, so scrolling, width and margins come for free. - let settings = adw::PreferencesPage::new(); + // settings: a tabbed preferences page — Identity, Server, Backup, + // Advanced — switched by an adw::ViewSwitcher over an + // adw::ViewStack, each tab its own PreferencesPage. + let settings_stack = adw::ViewStack::new(); - let identity_group = adw::PreferencesGroup::builder().title("Identity").build(); - let identity_actions = gtk::Box::new(Orientation::Horizontal, 6); - let rotate_button = icon_label_button("view-refresh-symbolic", "Rotate", ""); - let learn_button = button("Learn key", ""); - identity_actions.append(&learn_button); - identity_actions.append(&rotate_button); - identity_group.set_header_suffix(Some(&identity_actions)); - let mut identity_values = Vec::new(); - for title in ["Address", "Key", "Fingerprint", "Rotations"] { - let row = adw::ActionRow::builder().title(title).build(); - let value = label("", "mono dim"); - value.set_selectable(true); - value.set_ellipsize(gtk::pango::EllipsizeMode::End); - value.set_max_width_chars(48); - value.set_valign(Align::Center); - row.add_suffix(&value); + // Read-only values use Adwaita's "property" row: the field name + // sits small above the value it labels, which leaves the value + // itself room to wrap instead of being ellipsized into a suffix. + // Markup is off so the copy button can hand back exactly what is + // on screen. + let property_row = |title: &str| -> adw::ActionRow { + let row = adw::ActionRow::builder() + .title(title) + .use_markup(false) + .subtitle_lines(0) + .subtitle_selectable(true) + .build(); + row.add_css_class("property"); + row.add_css_class("monospace"); + let copy = icon_button( + "edit-copy-symbolic", + &format!("Copy the {}", title.to_lowercase()), + ); + let copy_source = row.clone(); + copy.connect_clicked(move |_| { + if let Some(display) = gtk::gdk::Display::default() { + display + .clipboard() + .set_text(©_source.subtitle().unwrap_or_default()); + } + }); + row.add_suffix(©); + row + }; + + // -- Identity tab -------------------------------------------------- + // The switcher already names each page, so the first group on a + // page carries no title of its own. + let identity_page = adw::PreferencesPage::new(); + let identity_group = adw::PreferencesGroup::new(); + let mut identity_rows = Vec::new(); + for title in ["Address", "Key", "Fingerprint", "URI"] { + let row = property_row(title); identity_group.add(&row); - identity_values.push(value); + identity_rows.push(row); } - settings.add(&identity_group); + identity_page.add(&identity_group); - // The pin offer is a group that appears in place rather than a - // dialog: pinning is a decision about this host, taken next to the - // identity it belongs to. - let offer_group = adw::PreferencesGroup::builder() - .title("Server key offered") - .visible(false) + // The group header names this one, so the row carries only the + // secret — masked until the reveal button is pressed. + let secret_group = adw::PreferencesGroup::builder() + .title("Recovery Secret") .build(); - let offer_row = adw::ActionRow::builder().title("").build(); - offer_row.add_css_class("monospace"); - offer_row.set_subtitle_lines(2); - let offer_pin = button("Pin", "accent"); - offer_pin.set_valign(Align::Center); - let offer_discard = button("Discard", ""); - offer_discard.set_valign(Align::Center); - offer_row.add_suffix(&offer_discard); - offer_row.add_suffix(&offer_pin); - offer_group.add(&offer_row); - settings.add(&offer_group); - - let pins_group = adw::PreferencesGroup::builder() - .title("Server keys") + let secret_row = adw::ActionRow::builder() + .use_markup(false) + .title_lines(0) + .title_selectable(true) .build(); - settings.add(&pins_group); + secret_row.add_css_class("monospace"); + let secret_reveal = icon_button("view-reveal-symbolic", "Reveal the recovery secret"); + let secret_copy = icon_button("edit-copy-symbolic", "Copy the recovery secret"); + secret_row.add_suffix(&secret_reveal); + secret_row.add_suffix(&secret_copy); + secret_group.add(&secret_row); + identity_page.add(&secret_group); + settings_stack.add_titled_with_icon( + &identity_page, + Some("identity"), + "Identity", + "avatar-default-symbolic", + ); - let fetch_group = adw::PreferencesGroup::builder().title("Fetching").build(); - let keep_mail_row = adw::SwitchRow::builder() - .title("Keep mail on the server") - .subtitle("Off: a fetch removes mail from the server once it's stored here.") - .build(); - fetch_group.add(&keep_mail_row); - settings.add(&fetch_group); - - let network_group = adw::PreferencesGroup::builder().title("Network").build(); + // -- Server tab ---------------------------------------------------- + let server_page = adw::PreferencesPage::new(); + let server_group = adw::PreferencesGroup::builder().title("Connection").build(); + let host_row = property_row("Host"); + let public_key_row = property_row("Public Key"); + server_group.add(&host_row); + server_group.add(&public_key_row); let timeout_row = adw::SpinRow::with_range(1.0, 600.0, 1.0); timeout_row.set_title("Timeout"); timeout_row.set_subtitle("Seconds before a network call gives up"); - network_group.add(&timeout_row); - settings.add(&network_group); + server_group.add(&timeout_row); + server_page.add(&server_group); + let network_group = adw::PreferencesGroup::builder().title("Fetching").build(); + let keep_mail_row = adw::ActionRow::builder() + .title("Leave mail on server") + .subtitle( + "Keep a copy on the server after fetching, so another client can \ + fetch it too. When this is enabled, deleting a message removes \ + it from this client and the server.", + ) + .subtitle_lines(0) + .activatable(true) + .build(); + let keep_mail_check = gtk::CheckButton::builder().valign(Align::Center).build(); + keep_mail_row.add_suffix(&keep_mail_check); + keep_mail_row.set_activatable_widget(Some(&keep_mail_check)); + network_group.add(&keep_mail_row); + server_page.add(&network_group); + settings_stack.add_titled_with_icon( + &server_page, + Some("server"), + "Server", + "network-server-symbolic", + ); + + // -- Backup tab ---------------------------------------------------- + let backup_page = adw::PreferencesPage::new(); let backup_group = adw::PreferencesGroup::builder() - .title("Backup") .description( - "Sealed to a key derived from the seed, so it restores only where this\n identity already lives. The seed itself is never in the file.", + "One file holding your mail, contacts and server info for moving \ + to another client, or for keeping what the server no longer \ + has. It is sealed to this identity and deliberately leaves the \ + secret itself out.", ) .build(); - let backup_actions = gtk::Box::new(Orientation::Horizontal, 6); - let export_button = icon_label_button("document-save-symbolic", "Export", ""); - let copy_export = icon_button("edit-copy-symbolic", "Copy the backup"); - backup_actions.append(©_export); - backup_actions.append(&export_button); - backup_group.set_header_suffix(Some(&backup_actions)); - let backup_label = label("", "mono caption"); - backup_label.set_wrap(true); - backup_label.set_wrap_mode(gtk::pango::WrapMode::Char); - backup_label.set_selectable(true); - let backup_scroll = gtk::ScrolledWindow::new(); - backup_scroll.add_css_class("card"); - backup_scroll.set_height_request(72); - backup_scroll.set_child(Some(&backup_label)); - backup_group.add(&backup_scroll); - settings.add(&backup_group); - - let import_group = adw::PreferencesGroup::builder() - .title("Restore") - .description("Paste a gsmol backup to restore the contacts, pins and mail it carries.") + let export_row = adw::ActionRow::builder() + .title("Export") + .subtitle("Write a backup file") + .activatable(true) .build(); - let import_view = gtk::TextView::new(); - import_view.set_wrap_mode(WrapMode::WordChar); - import_view.set_top_margin(8); - import_view.set_bottom_margin(8); - import_view.set_left_margin(8); - import_view.set_right_margin(8); - import_view.add_css_class("monospace"); - let import_body = import_view.buffer(); - let import_scroll = gtk::ScrolledWindow::new(); - import_scroll.add_css_class("card"); - import_scroll.set_height_request(96); - import_scroll.set_child(Some(&import_view)); - import_group.add(&import_scroll); - let import_backup = icon_label_button("document-open-symbolic", "Restore", ""); - import_backup.set_halign(Align::End); - import_backup.set_margin_top(6); - import_group.add(&import_backup); - settings.add(&import_group); + export_row.add_prefix(>k::Image::from_icon_name("document-save-symbolic")); + export_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + let import_row = adw::ActionRow::builder() + .title("Import") + .subtitle("Restore from a backup file") + .activatable(true) + .build(); + import_row.add_prefix(>k::Image::from_icon_name("document-open-symbolic")); + import_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); + backup_group.add(&export_row); + backup_group.add(&import_row); + backup_page.add(&backup_group); + settings_stack.add_titled_with_icon( + &backup_page, + Some("backup"), + "Backup", + "document-save-symbolic", + ); - // The reset closes the page list: nothing after it survives a - // wipe, so it sits last, marked destructive like the delete. - let reset_group = adw::PreferencesGroup::builder() - .title("Factory reset") + // -- Advanced tab -------------------------------------------------- + let advanced_page = adw::PreferencesPage::new(); + let rotations_group = adw::PreferencesGroup::builder() + .title("Rotations") .description( - "Erase the seed key, the sealed store and every sidecar on this device, and return to onboarding. Mail already on the server is not touched.", + "Issues a fresh key from your secret with a signed certificate \ + proving it replaces this one. Contacts accept the change from \ + that chain, and the old key stays readable. Rotation is not \ + revocation.", ) .build(); - let reset_button = icon_label_button("user-trash-symbolic", "Factory reset", "danger"); - reset_button.set_valign(Align::Center); - reset_group.set_header_suffix(Some(&reset_button)); - settings.add(&reset_group); + // Both actions sit in their group's body, centred under the + // explanation: a long description wraps under the group title and + // would run straight into a header-suffix button. + let rotations_row = adw::ActionRow::builder().title("Completed").build(); + let rotations_value = label("", "dim numeric"); + rotations_value.set_valign(Align::Center); + rotations_row.add_suffix(&rotations_value); + rotations_group.add(&rotations_row); + let rotate_button = icon_label_button("view-refresh-symbolic", "Rotate identity", ""); + rotate_button.set_halign(Align::Center); + rotate_button.set_margin_top(12); + rotations_group.add(&rotate_button); + advanced_page.add(&rotations_group); + + // The log-out button is wide and destructive, so it sits in the + // group's body under the warning rather than squeezed beside the + // header where its label would crowd the title. + let logout_group = adw::PreferencesGroup::builder() + .title("Log Out") + .description( + "Removes this identity from this client entirely: the secret, \ + pins, contacts and every cached message. Without a backup of \ + the secret there is no way back in. Mail already fetched here \ + may have no other copy anywhere, unless \"leave mail on \ + server\" was on. Whatever is still on the server comes back \ + on the first fetch after logging back in.", + ) + .build(); + let reset_button = icon_label_button( + "system-log-out-symbolic", + "Log out and delete all data", + "danger", + ); + reset_button.set_halign(Align::Center); + reset_button.set_margin_top(12); + logout_group.add(&reset_button); + advanced_page.add(&logout_group); + settings_stack.add_titled_with_icon( + &advanced_page, + Some("advanced"), + "Advanced", + "emblem-system-symbolic", + ); + + // The switcher is a centred bar of its own above the pages, since + // the window's headerbar already carries the app name and address. + let settings_switcher = adw::ViewSwitcher::builder() + .stack(&settings_stack) + .policy(adw::ViewSwitcherPolicy::Wide) + .halign(Align::Center) + .build(); + let settings_switcher_bar = gtk::Box::new(Orientation::Vertical, 0); + settings_switcher_bar.add_css_class("toolbar"); + settings_switcher_bar.append(&settings_switcher); + let settings = gtk::Box::new(Orientation::Vertical, 0); + settings.append(&settings_switcher_bar); + settings.append(&settings_stack); stack.add_named(&settings, Some("settings")); @@ -1013,7 +1101,6 @@ impl CarrierWidgets { }); own_row.connect_clicked(input(Message::OwnSelected)); - copy_export.connect_clicked(input(Message::CopyExport)); import_button.connect_clicked(input(Message::ContactImportPressed)); let import_sender = sender.clone(); contact_import.connect_changed(move |entry| { @@ -1026,22 +1113,57 @@ impl CarrierWidgets { rotate_button.connect_clicked(input(Message::RotatePressed)); reset_button.connect_clicked(input(Message::ResetPressed)); - learn_button.connect_clicked(input(Message::SettingsLearnKey)); - offer_pin.connect_clicked(input(Message::PinAccept)); - offer_discard.connect_clicked(input(Message::PinDiscard)); - export_button.connect_clicked(input(Message::ExportPressed)); - import_backup.connect_clicked(input(Message::ImportPressed)); + secret_reveal.connect_clicked(input(Message::SecretRevealToggled)); + secret_copy.connect_clicked(input(Message::CopySecret)); let timeout_sender = sender.clone(); timeout_row.connect_value_notify(move |row| { timeout_sender.input(Message::TimeoutChanged((row.value() as u64).to_string())) }); let keep_mail_sender = sender.clone(); - keep_mail_row.connect_active_notify(move |row| { - keep_mail_sender.input(Message::KeepMailChanged(row.is_active())) + keep_mail_check.connect_toggled(move |check| { + keep_mail_sender.input(Message::KeepMailChanged(check.is_active())) }); - let import_body_sender = sender.clone(); - import_body.connect_changed(move |buffer| { - import_body_sender.input(Message::ImportBodyChanged(buffer_text(buffer))) + // Export/Import go straight to a native file dialog rather than a + // Message — the chosen path only matters for this one call, and + // round-tripping it through the model would gain nothing. + let export_sender = sender.clone(); + let export_window = root.clone(); + export_row.connect_activated(move |_| { + let dialog = gtk::FileDialog::builder() + .title("Export Backup") + .initial_name("carrier-backup.gsmol") + .build(); + let sender = export_sender.clone(); + dialog.save( + Some(&export_window), + gtk::gio::Cancellable::NONE, + move |result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + sender.input(Message::ExportBackupTo(path)); + } + } + }, + ); + }); + let backup_import_sender = sender.clone(); + let backup_import_window = root.clone(); + import_row.connect_activated(move |_| { + let dialog = gtk::FileDialog::builder().title("Import Backup").build(); + let sender = backup_import_sender.clone(); + dialog.open( + Some(&backup_import_window), + gtk::gio::Cancellable::NONE, + move |result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + if let Ok(text) = std::fs::read_to_string(&path) { + sender.input(Message::ImportBackupFrom(text)); + } + } + } + }, + ); }); create_button.connect_clicked(input(Message::OnboardCreate)); @@ -1121,14 +1243,14 @@ impl CarrierWidgets { detail_share, detail_copy, detail_qr, - identity_values, - offer_group, - offer_row, - pins_group, - pin_rows: Vec::new(), - keep_mail_row, + identity_rows, + secret_row, + secret_reveal, + host_row, + public_key_row, + rotations_value, + keep_mail_check, timeout_row, - backup_label, onboard_seed_label, onboard_seed_entry, signup_key_button, @@ -1314,14 +1436,10 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { .map(|(index, key)| format!("rotated to index {index}; new key {}…", &key[..12])); let _ = out.send(OpResult::Generic(res)); }), - PendingOp::Unpin { label } => sender.spawn_command(move |out| { - let res = ops::unpin(&store, &label).map(|_| format!("unpinned {label}")); - let _ = out.send(OpResult::Generic(res)); - }), - PendingOp::Export { dir } => sender.spawn_command(move |out| { - let exported = ops::export_backup(&store, &master, &dir) - .map(|(path, text)| (format!("backup written to {}", path.display()), text)); - let _ = out.send(OpResult::Exported(exported)); + PendingOp::Export { path } => sender.spawn_command(move |out| { + let _ = out.send(OpResult::Generic(ops::export_backup( + &store, &master, &path, + ))); }), PendingOp::ImportBackup { text } => sender.spawn_command(move |out| { let _ = out.send(OpResult::Generic(ops::import_backup( @@ -1690,62 +1808,48 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender()) - .unwrap_or_default(); - w.backup_label.set_text(&backup_text); // A programmatic reset (factory reset, discarding a cancelled // identity) clears these in the model; push that once into the @@ -1835,7 +1939,7 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let alert = adw::AlertDialog::new( Some("Rotate the identity?"), - Some("The old key stays derivable from the seed, but the rotation index never moves back."), + Some("Issues a fresh key from your secret with a signed certificate proving it replaces this one. Contacts accept the change from that chain, and the old key stays readable. Rotation is not revocation."), ); alert.add_response("cancel", "Cancel"); alert.add_response("rotate", "Rotate"); @@ -1844,11 +1948,11 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let alert = adw::AlertDialog::new( - Some("Factory reset this device?"), - Some("The seed key, the sealed store, contacts, server pins, drafts and read marks are erased here. Mail already on the server stays there — without the seed it cannot be read back."), + Some("Log out and delete all data?"), + Some("Removes this identity from this client entirely: the secret, pins, contacts and every cached message. Without a backup of the secret there is no way back in. Mail already fetched here may have no other copy anywhere, unless \"leave mail on server\" was on. Whatever is still on the server comes back on the first fetch after logging back in."), ); alert.add_response("cancel", "Cancel"); - alert.add_response("reset", "Factory reset"); + alert.add_response("reset", "Log Out"); alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive); alert } From 4c5665a6ff754420523c55d1eff56f7e0a31737a Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 21:36:02 +0300 Subject: [PATCH 25/37] feat: gate first contact on a pinned server, confirm key changes --- README.md | 7 +- src/app.rs | 557 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/demo.rs | 5 + src/ops.rs | 197 ++++++++++++++++++- src/view.rs | 259 +++++++++++++++++++++--- 5 files changed, 981 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 529485a..82198b3 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ A minimalist desktop client for [Smol Mail](../smolmail): one compact window — - Composes closed by any sidebar trip are stashed as drafts — kept beside the store, reopened from a folder that appears only once one exists; Cancel is the only way to throw one away. - Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body; replying to such a mail asks whether to carry copies of the fields into the reply, where they stay editable. - Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. -- Contacts with verified/tofu marks and smol:// import; your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. -- Key rotation with a confirm-first step, server pin management, a network timeout, gsmol backup export/import that never contains the seed, and a factory reset that erases the local files and returns to onboarding. +- Contacts with verified/tofu marks and smol:// import; each contact's detail names the server its address lives on and whether that server's key is pinned, with a pin action when it is not. Your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. +- Optional strict first contact: writing to someone new whose address carries no key means their key has to be looked up on their own server, and an unpinned server can answer with one of its own. With the setting on, that send stops and the compose offers to fetch and pin the server's key first, so it can be checked against what the operator published. A smol:// address whose key differs from the one already on file asks before replacing it, either way. +- Key rotation with a confirm-first step, a network timeout, gsmol backup export/import that never contains the seed, and a log-out that erases the local files and returns to onboarding. Preferences live in a plaintext sidecar beside the store, so they survive a restart. ## Layout @@ -24,7 +25,7 @@ src/ops.rs fumi-core calls, key/read-mark files, and the offline tests ## State -Everything lives under `$XDG_CONFIG_HOME/carrier` (default `~/.config/carrier`): `carrier.key` is the master seed, written 0600; `carrier.db` is fumi-core's sealed store; `read.txt` is carrier's own read marks, one hex id per line. +Everything lives under `$XDG_CONFIG_HOME/carrier` (default `~/.config/carrier`): `carrier.key` is the master seed, written 0600; `carrier.db` is fumi-core's sealed store; `read.txt` is carrier's own read marks, one hex id per line; `settings.txt` holds the preferences as one `key value` line each. ## Run and verify diff --git a/src/app.rs b/src/app.rs index 5824a81..9cd9dee 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,8 +10,8 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use fumi_core::address::Address; -use fumi_core::crypto::{unb32, KEY_LEN}; +use fumi_core::address::{trust_label, Address, Scheme}; +use fumi_core::crypto::{fingerprint, unb32, KEY_LEN}; use fumi_core::store::Store; use gtk::prelude::*; @@ -111,6 +111,20 @@ pub struct Compose { pub headers: Vec, } +/// A correspondent's server that has to be pinned before carrier will +/// take their key from it, and the key fetched for it so far. The compose +/// and the contact detail both prompt through this one slot: they are +/// never on screen together, since selecting a contact stashes the +/// compose. Signup keeps its own `pin_offer` — there the address arrives +/// late from a live entry, and "already pinned" is a third state this +/// struct has no room for. +#[derive(Debug, Clone)] +pub struct PinPrompt { + pub addr: Address, + /// None until the fetch lands, Some once the key is on screen. + pub offered: Option<[u8; KEY_LEN]>, +} + #[derive(Debug, Clone)] pub enum Dialog { /// Delete locally, and on the server too when `everywhere` is chosen. @@ -131,6 +145,49 @@ pub enum Dialog { id: String, rows: Vec, }, + /// A `smol://` address whose key is not the one on file for that + /// contact. `save_contact` is an upsert and both ways in use it with + /// `verified = true`, so a replacement would silently displace a + /// known-good key and skip fumi's own rotation-chain check. It is + /// confirmed here instead. + KeyChange { + address: String, + offered: [u8; KEY_LEN], + known_fingerprint: String, + offered_fingerprint: String, + resume: KeyChangeResume, + }, +} + +/// What confirming a key change goes on to do. +#[derive(Debug, Clone)] +pub enum KeyChangeResume { + Send, + Import { uri: String }, +} + +/// Which question a dialog is asking. The alert's response demux keys off +/// this instead of one boolean per variant, so a new dialog adds arms +/// rather than widening a tuple. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DialogKind { + Delete, + Rotate, + Reset, + Reply, + KeyChange, +} + +impl Dialog { + pub fn kind(&self) -> DialogKind { + match self { + Dialog::Delete { .. } => DialogKind::Delete, + Dialog::Rotate => DialogKind::Rotate, + Dialog::Reset => DialogKind::Reset, + Dialog::Reply { .. } => DialogKind::Reply, + Dialog::KeyChange { .. } => DialogKind::KeyChange, + } + } } /// UI inputs. Background results arrive as [`OpResult`] instead, so the @@ -177,6 +234,10 @@ pub enum Message { ComposeHeaderRemoved(usize), ComposeClosed, SendPressed, + /// The pin card's one button, on whichever surface shows it: fetches + /// the server's key, then — once it is on screen — pins it, and in a + /// compose sends in the same press. + PinServerAction, ReplyPressed, ReplyCancelled, ReplyConfirmed(bool), @@ -203,6 +264,7 @@ pub enum Message { SecretRevealToggled, CopySecret, KeepMailChanged(bool), + StrictPinningChanged(bool), TimeoutChanged(String), RotatePressed, RotateConfirmed, @@ -212,6 +274,8 @@ pub enum Message { ResetPressed, ResetConfirmed, ResetCancelled, + KeyChangeConfirmed, + KeyChangeCancelled, } /// What a background operation reports back. @@ -293,6 +357,10 @@ pub struct Carrier { /// Off by default: a fetch acknowledges what it takes, so mail is /// removed from the server once it's stored here. pub keep_mail: bool, + /// Refuse a send that would take a recipient's key from a server with + /// no pin: `client::resolve` verifies nothing, so such a key is + /// whatever answered the connection. + pub strict_pinning: bool, pub timeout: u64, pub timeout_text: String, pub onboard: Onboard, @@ -310,7 +378,10 @@ pub struct Carrier { /// the drafts sidecar; the folder only shows once it holds one. pub drafts: Vec, pub snapshot: Option, + /// Signup's offer only; correspondents' servers prompt through + /// `pin_prompt`. pub pin_offer: Option<(Address, [u8; KEY_LEN])>, + pub pin_prompt: Option, /// The Identity tab's recovery secret is masked until this is toggled. pub secret_revealed: bool, pub contact_import: String, @@ -343,6 +414,7 @@ impl Carrier { (true, true) => Page::Mail, }; let drafts = ops::load_drafts(&dir.join("drafts.txt")); + let settings = ops::load_settings(&dir.join("settings.txt")); let app = Carrier { marks: ReadMarks::load(dir.join("read.txt")), dir, @@ -353,9 +425,10 @@ impl Carrier { status_error: false, busy: None, cancel: Arc::new(AtomicBool::new(false)), - keep_mail: false, - timeout: 30, - timeout_text: "30".to_string(), + keep_mail: settings.keep_mail, + strict_pinning: settings.strict_pinning, + timeout: settings.timeout, + timeout_text: settings.timeout.to_string(), onboard: Onboard { mode: OnboardMode::Start, seed: String::new(), @@ -378,6 +451,7 @@ impl Carrier { drafts, snapshot: None, pin_offer: None, + pin_prompt: None, secret_revealed: false, contact_import: String::new(), dialog: None, @@ -402,6 +476,19 @@ impl Carrier { self.dir.join("drafts.txt") } + /// Rewritten on every change: three values in one short file, the + /// same whole-file rewrite the drafts sidecar does. + fn save_settings(&self) { + ops::save_settings( + &self.dir.join("settings.txt"), + &ops::Settings { + keep_mail: self.keep_mail, + timeout: self.timeout, + strict_pinning: self.strict_pinning, + }, + ); + } + /// Every sidebar trip closes the compose this way: a compose with /// anything in it becomes a draft, an empty one is thrown away, and /// Cancel stays the only way to discard work outright. @@ -575,6 +662,49 @@ impl Carrier { Address::parse(&format!("{user}@{host}")).ok() } + /// The recipient's address when sending to it would make carrier take + /// their key from a server it has never pinned: no key in the address, + /// no contact on file, no pin for the host. A pin is enough to make + /// the lookup trustworthy — `connect` rejects a mismatch whatever + /// `require_pin` says. RNS has nothing to pin: there the destination + /// hash in the address is itself the pin. + fn unpinned_first_contact(&self, to: &str) -> Option
{ + let addr = Address::parse(to).ok()?; + if addr.scheme != Scheme::Tcp || addr.identity.is_some() { + return None; + } + // A store error reads as "not known, not pinned": this gate errs + // towards asking. + let known = self.store.contact(&addr.short()).ok().flatten().is_some(); + let pinned = self + .store + .server_pin(&addr.host, addr.port) + .ok() + .flatten() + .is_some(); + (!known && !pinned).then_some(addr) + } + + /// A self-certifying address whose key differs from the contact on + /// file. Both keys are public and about to be shown side by side, so + /// a plain comparison is all this needs. + fn key_change(&self, text: &str) -> Option<(String, [u8; KEY_LEN], [u8; KEY_LEN])> { + let addr = Address::parse(text).ok()?; + let offered = addr.identity?; + let (known, _) = self.store.contact(&addr.short()).ok().flatten()?; + (known != offered).then_some((addr.short(), known, offered)) + } + + /// The key the pin card fetched, if it was fetched for this very + /// recipient; taken, so editing the address cannot reuse it. + fn take_offered_pin(&mut self, to: &str) -> Option<(Address, [u8; KEY_LEN])> { + let wanted = Address::parse(to).ok()?.short(); + let prompt = self.pin_prompt.as_ref()?; + let key = prompt.offered.filter(|_| prompt.addr.short() == wanted)?; + let prompt = self.pin_prompt.take()?; + Some((prompt.addr, key)) + } + /// The host as typed on the signup page's server step, before a /// username exists — `learn_key`/`connect` only read host/port/scheme, /// so a throwaway username satisfies `Address::parse`'s grammar. @@ -688,6 +818,11 @@ impl Carrier { } Message::KeepMailChanged(keep) => { self.keep_mail = keep; + self.save_settings(); + } + Message::StrictPinningChanged(strict) => { + self.strict_pinning = strict; + self.save_settings(); } Message::SecretRevealToggled => { self.secret_revealed = !self.secret_revealed; @@ -695,6 +830,7 @@ impl Carrier { Message::TimeoutChanged(text) => { if let Ok(timeout) = text.trim().parse::() { self.timeout = timeout; + self.save_settings(); } self.timeout_text = text; } @@ -939,16 +1075,103 @@ impl Carrier { self.fail(reason); return Vec::new(); } + let to = compose.to.trim().to_string(); + // Asked before anything else: a pasted address carrying a + // different key is a question about who the recipient is, + // which has to settle before how to reach them. + if let Some((address, known, offered)) = self.key_change(&to) { + self.dialog = Some(Dialog::KeyChange { + address, + offered, + known_fingerprint: fingerprint(&known), + offered_fingerprint: fingerprint(&offered), + resume: KeyChangeResume::Send, + }); + return Vec::new(); + } let op = PendingOp::Send { - to: compose.to.trim().to_string(), + to: to.clone(), subject: compose.subject.trim().to_string(), body: compose.body.clone(), reply_to: compose.reply_to.clone(), headers, }; + // The card's second press: the key on screen was fetched + // for this recipient's server, so pin it here — a local + // write — and the send below opens a pinned session. + // Writing the pin first is also what stops the re-entry + // in PinServerAction from looping back into the gate. + if let Some((addr, key)) = self.take_offered_pin(&to) { + if let Err(e) = ops::pin(&self.store, &addr, &key) { + self.fail(e); + return Vec::new(); + } + } + if self.strict_pinning { + if let Some(addr) = self.unpinned_first_contact(&to) { + self.fail(format!( + "{} is not a contact yet and {} is not pinned — pin their server's key to send.", + addr.short(), + trust_label(&addr.host, addr.port), + )); + self.pin_prompt = Some(PinPrompt { + addr, + offered: None, + }); + // The compose stays open: its text is the message + // the user is still trying to send. + return Vec::new(); + } + } + self.pin_prompt = None; self.compose = None; return vec![op]; } + Message::PinServerAction => { + // The surface is the address: a compose's recipient, or + // the contact whose detail is open. Never both at once. + let Some(to) = self + .compose + .as_ref() + .map(|compose| compose.to.trim().to_string()) + .or_else(|| self.selected_contact.clone()) + else { + return Vec::new(); + }; + let Ok(addr) = Address::parse(&to) else { + self.fail("that address does not parse".into()); + return Vec::new(); + }; + let fetched = self + .pin_prompt + .as_ref() + .is_some_and(|p| p.offered.is_some() && p.addr.short() == addr.short()); + if !fetched { + self.pin_prompt = Some(PinPrompt { + addr: addr.clone(), + offered: None, + }); + return vec![PendingOp::LearnKey { addr }]; + } + // In a compose the pin is only ever the first half of the + // send the user already asked for, and SendPressed writes + // it before re-checking the gate — so this does not loop. + if self.compose.is_some() { + return self.handle(Message::SendPressed); + } + let Some(prompt) = self.pin_prompt.take() else { + return Vec::new(); + }; + return prompt + .offered + .map(|key| { + vec![PendingOp::Pin { + addr: prompt.addr, + key, + }] + }) + .unwrap_or_default(); + } Message::DeletePressed => { let Some((folder, msg)) = &self.open else { return Vec::new(); @@ -1019,8 +1242,45 @@ impl Carrier { if uri.is_empty() { return Vec::new(); } + if let Some((address, known, offered)) = self.key_change(&uri) { + self.dialog = Some(Dialog::KeyChange { + address, + offered, + known_fingerprint: fingerprint(&known), + offered_fingerprint: fingerprint(&offered), + resume: KeyChangeResume::Import { uri }, + }); + return Vec::new(); + } return vec![PendingOp::ImportContact { uri }]; } + Message::KeyChangeCancelled => { + self.dialog = None; + } + Message::KeyChangeConfirmed => { + let Some(Dialog::KeyChange { + address, + offered, + resume, + .. + }) = self.dialog.take() + else { + return Vec::new(); + }; + match resume { + // Recording the accepted key is also what keeps the + // re-dispatch below from asking the same question. + KeyChangeResume::Send => { + match ops::save_contact(&self.store, &address, &offered) { + Ok(()) => return self.handle(Message::SendPressed), + Err(e) => self.fail(e), + } + } + KeyChangeResume::Import { uri } => { + return vec![PendingOp::ImportContact { uri }] + } + } + } Message::ContactWrite(address) => { let key = self .snapshot @@ -1143,7 +1403,17 @@ impl Carrier { } OpResult::PinOffered(Ok(Some((addr, key)))) => { self.done(); - self.pin_offer = Some((addr, key)); + // The prompt is armed synchronously before the fetch is + // ever spawned, so an offer that matches it belongs to a + // correspondent's server; anything else is signup's. + match self + .pin_prompt + .as_mut() + .filter(|prompt| prompt.addr.short() == addr.short()) + { + Some(prompt) => prompt.offered = Some(key), + None => self.pin_offer = Some((addr, key)), + } self.note( "The server presented this key. Pin it only if the fingerprint \ matches what the operator published." @@ -1153,7 +1423,11 @@ impl Carrier { } OpResult::PinOffered(Ok(None)) => { self.done(); - self.signup.key_trusted = true; + // A pin appeared in the meantime: a correspondent prompt + // has nothing left to ask, and signup can advance. + if self.pin_prompt.take().is_none() { + self.signup.key_trusted = true; + } self.note("This server's key is already pinned.".into()); None } @@ -1165,6 +1439,12 @@ impl Carrier { OpResult::Pinned(Ok(())) => { self.done(); self.note("server key pinned".into()); + // The contact detail shows the pin state, so that pane + // needs the fresh row; signup's own pin changes nothing + // that is on screen. + if self.page == Page::Mail { + return self.refresh(); + } None } OpResult::Pinned(Err(e)) => { @@ -1230,6 +1510,261 @@ mod tests { assert!(!app.keep_mail); } + fn composing(to: &str) -> Compose { + Compose { + to: to.into(), + subject: "hello".into(), + body: "a message".into(), + reply_to: None, + headers: Vec::new(), + } + } + + /// `client::resolve` verifies nothing, so a first contact over a + /// server with no pin seals the message to whatever answered. Strict + /// mode stops exactly that case, and keeps the compose so the text + /// the user is trying to send survives the refusal. + #[test] + fn strict_pinning_blocks_first_contact_over_an_unpinned_server() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.strict_pinning = true; + app.compose = Some(composing("bob@example.org")); + + assert!(app.handle(Message::SendPressed).is_empty()); + assert!(app.compose.is_some(), "the draft must survive the refusal"); + assert!(app.status_error); + let prompt = app.pin_prompt.as_ref().expect("the card must be armed"); + assert_eq!(prompt.addr.short(), "bob@example.org"); + assert!(prompt.offered.is_none()); + + // A pin is all the gate wants: `connect` rejects a mismatch + // whatever it was asked for, so the lookup is now trustworthy. + app.store + .pin_server("example.org", 1961, &[7u8; KEY_LEN]) + .unwrap(); + let ops = app.handle(Message::SendPressed); + assert!(matches!(ops.as_slice(), [PendingOp::Send { .. }])); + assert!(app.compose.is_none()); + } + + /// The gate only bites where a key would have to be learned: an + /// address that carries one, a contact already on file, and RNS — + /// where the destination hash is itself the pin — all go straight out. + #[test] + fn strict_pinning_lets_carried_keys_contacts_and_rns_through() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.strict_pinning = true; + let key = [3u8; KEY_LEN]; + + let carried = fumi_core::crypto::b32(&key); + app.compose = Some(composing(&format!("smol://bob@example.org/{carried}"))); + assert!(matches!( + app.handle(Message::SendPressed).as_slice(), + [PendingOp::Send { .. }] + )); + + app.store + .save_contact("carol@example.org", &key, false) + .unwrap(); + app.compose = Some(composing("carol@example.org")); + assert!(matches!( + app.handle(Message::SendPressed).as_slice(), + [PendingOp::Send { .. }] + )); + + let hash = "8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a"; + app.compose = Some(composing(&format!("smol+rns://dave@{hash}"))); + assert!( + matches!( + app.handle(Message::SendPressed).as_slice(), + [PendingOp::Send { .. }] + ), + "rns has nothing to pin, so the gate must not strand it" + ); + } + + #[test] + fn strict_pinning_off_sends_over_an_unpinned_server() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.handle(Message::StrictPinningChanged(false)); + app.compose = Some(composing("bob@example.org")); + assert!(matches!( + app.handle(Message::SendPressed).as_slice(), + [PendingOp::Send { .. }] + )); + } + + /// The card's two presses: fetch, then pin and send in one go. The + /// offer must land in `pin_prompt` and leave signup's own slot alone, + /// and the pin must be written before the send is dispatched — that + /// ordering is what keeps the re-entry from looping. + #[test] + fn the_pin_card_fetches_then_pins_and_sends() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.strict_pinning = true; + app.compose = Some(composing("bob@example.org")); + app.handle(Message::SendPressed); + + let ops = app.handle(Message::PinServerAction); + assert!(matches!(ops.as_slice(), [PendingOp::LearnKey { .. }])); + + let addr = Address::parse("bob@example.org").unwrap(); + let key = [5u8; KEY_LEN]; + app.handle_result(OpResult::PinOffered(Ok(Some((addr, key))))); + assert!(app.pin_offer.is_none(), "signup's slot must stay empty"); + assert_eq!(app.pin_prompt.as_ref().unwrap().offered, Some(key)); + + let ops = app.handle(Message::PinServerAction); + assert!(matches!(ops.as_slice(), [PendingOp::Send { .. }])); + assert_eq!( + app.store.server_pin("example.org", 1961).unwrap(), + Some(key) + ); + assert!(app.pin_prompt.is_none()); + assert!(app.compose.is_none()); + } + + /// A pin that appeared in the meantime retires the prompt without + /// touching signup, which shares the result variant. + #[test] + fn an_already_pinned_server_retires_the_prompt() { + let mut app = app(); + app.pin_prompt = Some(PinPrompt { + addr: Address::parse("bob@example.org").unwrap(), + offered: None, + }); + app.handle_result(OpResult::PinOffered(Ok(None))); + assert!(app.pin_prompt.is_none()); + assert!(!app.signup.key_trusted); + + app.handle_result(OpResult::PinOffered(Ok(None))); + assert!(app.signup.key_trusted, "an unarmed offer is signup's"); + } + + /// `save_contact` is an upsert, so a pasted address carrying a + /// different key would quietly displace a known-good one and skip + /// fumi's own rotation-chain check. It has to be confirmed. + #[test] + fn a_changed_smol_key_asks_before_replacing_a_contact() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + let known = [1u8; KEY_LEN]; + let swapped = [2u8; KEY_LEN]; + app.store + .save_contact("bob@example.org", &known, true) + .unwrap(); + let uri = format!( + "smol://bob@example.org/{}", + fumi_core::crypto::b32(&swapped) + ); + + app.compose = Some(composing(&uri)); + assert!(app.handle(Message::SendPressed).is_empty()); + assert!(matches!( + app.dialog, + Some(Dialog::KeyChange { + resume: KeyChangeResume::Send, + .. + }) + )); + + // Cancelling leaves both the draft and the stored key alone, and + // pressing Send again asks the same question. + app.handle(Message::KeyChangeCancelled); + assert!(app.dialog.is_none()); + assert!(app.compose.is_some()); + assert_eq!( + app.store.contact("bob@example.org").unwrap().unwrap().0, + known + ); + assert!(app.handle(Message::SendPressed).is_empty()); + + let ops = app.handle(Message::KeyChangeConfirmed); + assert!(matches!(ops.as_slice(), [PendingOp::Send { .. }])); + assert_eq!( + app.store.contact("bob@example.org").unwrap().unwrap().0, + swapped + ); + assert!(app.compose.is_none()); + + // The same address carrying the key already on file is not a + // change, so it goes straight out. + app.compose = Some(composing(&uri)); + assert!(matches!( + app.handle(Message::SendPressed).as_slice(), + [PendingOp::Send { .. }] + )); + } + + #[test] + fn a_changed_smol_key_asks_before_importing_it() { + let mut app = app(); + let known = [1u8; KEY_LEN]; + let swapped = [2u8; KEY_LEN]; + app.store + .save_contact("bob@example.org", &known, true) + .unwrap(); + + app.contact_import = format!( + "smol://bob@example.org/{}", + fumi_core::crypto::b32(&swapped) + ); + assert!(app.handle(Message::ContactImportPressed).is_empty()); + assert!(matches!( + app.dialog, + Some(Dialog::KeyChange { + resume: KeyChangeResume::Import { .. }, + .. + }) + )); + assert!(matches!( + app.handle(Message::KeyChangeConfirmed).as_slice(), + [PendingOp::ImportContact { .. }] + )); + + // An address with no contact on file is not a change either. + app.contact_import = format!("smol://new@example.org/{}", fumi_core::crypto::b32(&known)); + assert!(matches!( + app.handle(Message::ContactImportPressed).as_slice(), + [PendingOp::ImportContact { .. }] + )); + } + + /// Off by default — this one is opt-in — and a plain toggle, not + /// busy-guarded like a network action. + #[test] + fn strict_pinning_defaults_off_and_toggles() { + let mut app = app(); + assert!(!app.strict_pinning); + app.busy = Some("fetching".into()); + app.handle(Message::StrictPinningChanged(true)); + assert!(app.strict_pinning); + app.handle(Message::StrictPinningChanged(false)); + assert!(!app.strict_pinning); + } + + /// Settings are the one piece of state a restart used to forget, + /// which would have let a security toggle lapse on every launch. + #[test] + fn settings_survive_a_reboot() { + let dir = tempfile::tempdir().unwrap().keep(); + let (mut app, _) = Carrier::boot(dir.clone()); + app.handle(Message::StrictPinningChanged(true)); + app.handle(Message::KeepMailChanged(true)); + app.handle(Message::TimeoutChanged("90".into())); + assert!(dir.join("settings.txt").exists()); + + let (rebooted, _) = Carrier::boot(dir); + assert!(rebooted.strict_pinning); + assert!(rebooted.keep_mail); + assert_eq!(rebooted.timeout, 90); + assert_eq!(rebooted.timeout_text, "90"); + } + #[test] fn secret_reveal_toggles() { let mut app = app(); @@ -1271,6 +1806,7 @@ mod tests { std::fs::write(dir.join("drafts.txt"), "draft2 id 1 -\n\n\n\n").unwrap(); app.signup.host = "typed-before-the-reset.example".into(); app.signup.user = "alice".into(); + app.handle(Message::StrictPinningChanged(true)); // The button is busy-guarded like every other action. app.busy = Some("fetching".into()); @@ -1294,9 +1830,12 @@ mod tests { assert!(app.signup.host.is_empty()); assert!(app.signup.user.is_empty()); assert!(app.form_fresh.get()); - for name in ["carrier.key", "read.txt", "drafts.txt"] { + for name in ["carrier.key", "read.txt", "drafts.txt", "settings.txt"] { assert!(!dir.join(name).exists(), "{name} survived the reset"); } + // Preferences go with everything else, so the rebooted model is + // back to its defaults rather than the logged-out user's. + assert!(!app.strict_pinning); // The store is reopened by boot, so a fresh empty db is the one // file a reset legitimately leaves behind. assert!(dir.join("carrier.db").exists()); diff --git a/src/demo.rs b/src/demo.rs index 9c0bfba..e21619d 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -39,6 +39,11 @@ fn plant(dir: &Path) -> Result<(), String> { let me = Account::new(master, 0).map_err(|e| e.to_string())?; let addr = Address::parse("demo@carrier.local").map_err(|e| e.to_string())?; store.set_account(&addr).map_err(|e| e.to_string())?; + // Our own server is pinned, the contacts' server is not: the two + // states the trust lines and the pin prompts distinguish. + store + .pin_server(&addr.host, addr.port, &identity(42).pk()) + .map_err(|e| e.to_string())?; let pk = me.me().pk(); let mut read_ids: Vec = Vec::new(); diff --git a/src/ops.rs b/src/ops.rs index 916e826..7764681 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -7,7 +7,7 @@ use std::path::Path; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use fumi_core::address::Address; +use fumi_core::address::{trust_label, Address, Scheme}; use fumi_core::client::{FetchOptions, TrustChange}; use fumi_core::crypto::{b32, fingerprint, unb32, KEY_LEN}; use fumi_core::error::Error; @@ -52,12 +52,21 @@ pub struct Contact { pub verified: bool, /// Some(true) accepted, Some(false) blocked, None never decided. pub accepted: Option, + /// The server this address lives on, as the label pins are keyed by. + pub server: String, + /// Whether that server's key is pinned. A key learned over a session + /// with no pin is only as good as whoever answered, which `accepted` + /// stops saying anything about once it is Some. + pub pinned: bool, } #[derive(Debug, Clone)] pub struct Who { pub address: String, pub host: String, + /// Paired with `host` to build the label pins are keyed by, so the + /// account's own pin can be told apart from a correspondent's. + pub port: u16, pub key: String, pub fingerprint: String, pub uri: String, @@ -235,6 +244,74 @@ pub fn load_drafts(path: &Path) -> Vec { drafts } +/// Carrier's own preferences, beside the store as one `key value` line +/// each: nothing here is secret, and plaintext keeps a hand edit honest. +/// Unknown keys and unreadable values are skipped, so a file from another +/// build — or a torn write — costs only the lines it broke. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Settings { + pub keep_mail: bool, + pub timeout: u64, + /// Refuse to learn a recipient's key from a server with no pin. + pub strict_pinning: bool, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + keep_mail: false, + timeout: 30, + strict_pinning: false, + } + } +} + +fn flag(value: &str) -> Option { + match value.trim() { + "1" => Some(true), + "0" => Some(false), + _ => None, + } +} + +pub fn load_settings(path: &Path) -> Settings { + let mut settings = Settings::default(); + let text = std::fs::read_to_string(path).unwrap_or_default(); + for row in text.lines() { + let Some((key, value)) = row.split_once(' ') else { + continue; + }; + match key.trim() { + "keep_mail" => settings.keep_mail = flag(value).unwrap_or(settings.keep_mail), + "strict_pinning" => { + settings.strict_pinning = flag(value).unwrap_or(settings.strict_pinning) + } + // Out of the spin row's range is as good as unreadable: the + // widget would clamp it and write the clamp straight back. + "timeout" => { + settings.timeout = value + .trim() + .parse() + .ok() + .filter(|seconds| (1..=600).contains(seconds)) + .unwrap_or(settings.timeout) + } + _ => {} + } + } + settings +} + +pub fn save_settings(path: &Path, settings: &Settings) { + let text = format!( + "keep_mail {}\nstrict_pinning {}\ntimeout {}\n", + u8::from(settings.keep_mail), + u8::from(settings.strict_pinning), + settings.timeout, + ); + let _ = std::fs::write(path, text); +} + fn explain(e: Error) -> String { e.to_string() } @@ -310,7 +387,13 @@ pub fn delete_key(path: &Path) -> Result<(), String> { /// The caller must close the store first: Windows refuses to remove a /// file a live connection holds. pub fn factory_reset(dir: &Path) -> Result<(), String> { - for name in ["carrier.db", "carrier.key", "read.txt", "drafts.txt"] { + for name in [ + "carrier.db", + "carrier.key", + "read.txt", + "drafts.txt", + "settings.txt", + ] { let path = dir.join(name); if let Err(e) = std::fs::remove_file(&path) { if e.kind() != std::io::ErrorKind::NotFound { @@ -574,6 +657,14 @@ pub fn block_contact( } /// Adds a contact from a self-certifying `smol://` address. +/// Saves a key the user has confirmed, verified because it came from a +/// self-certifying address they chose to accept. +pub fn save_contact(store: &Store, address: &str, key: &[u8; KEY_LEN]) -> Result<(), String> { + store.save_contact(address, key, true).map_err(explain) +} + +/// A plain upsert: a key that would displace a known-good one is caught +/// at the app layer, which asks before this runs. pub fn import_contact(store: &Store, uri: &str) -> Result<(String, String), String> { let addr = parse_address(uri)?; let key = addr @@ -590,12 +681,31 @@ pub fn contacts(store: &Store) -> Result, String> { .contact_rows() .map_err(explain)? .into_iter() - .map(|(address, identity, verified, accepted)| Contact { - address, - key: b32(&identity), - fingerprint: fingerprint(&identity), - verified, - accepted: accepted.map(|state| state == 1), + .map(|(address, identity, verified, accepted)| { + let (server, pinned) = match Address::parse(&address) { + // On RNS the destination hash in the address is itself + // the pin: there is nothing to pin and nothing to warn + // about. + Ok(addr) if addr.scheme == Scheme::Rns => (addr.host.clone(), true), + Ok(addr) => ( + trust_label(&addr.host, addr.port), + store + .server_pin(&addr.host, addr.port) + .ok() + .flatten() + .is_some(), + ), + Err(_) => (String::new(), false), + }; + Contact { + address, + key: b32(&identity), + fingerprint: fingerprint(&identity), + verified, + accepted: accepted.map(|state| state == 1), + server, + pinned, + } }) .collect()) } @@ -727,6 +837,7 @@ pub fn whoami(store: &Store, master: &[u8; KEY_LEN]) -> Result { .as_ref() .map(|addr| addr.host.clone()) .unwrap_or_default(); + let port = account.as_ref().map(|addr| addr.port).unwrap_or_default(); let uri = account .as_ref() .map(|addr| addr.uri(&pk)) @@ -734,6 +845,7 @@ pub fn whoami(store: &Store, master: &[u8; KEY_LEN]) -> Result { Ok(Who { address, host, + port, key: b32(&pk), fingerprint: fingerprint(&pk), uri, @@ -778,6 +890,40 @@ mod tests { use fumi_core::message::{build_frontmatter, message_id, seal}; use fumi_core::store::now; + /// A settings file from another build, or one a hand edit broke, has + /// to degrade a line at a time rather than resetting the lot. + #[test] + fn settings_round_trip_and_degrade_per_line() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("settings.txt"); + assert_eq!(load_settings(&path), Settings::default()); + assert!(!Settings::default().strict_pinning); + + let settings = Settings { + keep_mail: true, + timeout: 90, + strict_pinning: true, + }; + save_settings(&path, &settings); + assert_eq!(load_settings(&path), settings); + + // Each unreadable value keeps the default; the readable one still + // lands, and an unknown key is simply skipped. + std::fs::write( + &path, + "keep_mail yes\ntimeout 0\nstrict_pinning 1\nwhat even 3\n", + ) + .unwrap(); + let degraded = load_settings(&path); + assert!(!degraded.keep_mail); + assert_eq!(degraded.timeout, 30); + assert!(degraded.strict_pinning); + + // Out of the spin row's range counts as unreadable. + std::fs::write(&path, "timeout 9999\n").unwrap(); + assert_eq!(load_settings(&path).timeout, 30); + } + #[test] fn drafts_round_trip_through_the_sidecar() { let dir = tempfile::tempdir().unwrap(); @@ -1029,6 +1175,41 @@ mod tests { assert_eq!(folders[0][0].subject, "kept"); } + /// The detail pane says whether a contact's key could have come from + /// an unpinned server, so each row carries its server's pin state — + /// keyed by the same label pins use, non-default ports included. + #[test] + fn contacts_report_their_server_and_pin_state() { + let store = setup(); + let key = master(); + store.save_contact("bob@example.org", &key, false).unwrap(); + store + .save_contact("eve@example.org:1962", &key, false) + .unwrap(); + + let by_address = |all: Vec, address: &str| { + all.into_iter().find(|c| c.address == address).unwrap() + }; + let all = contacts(&store).unwrap(); + assert_eq!( + by_address(all.clone(), "bob@example.org").server, + "example.org" + ); + assert!(!by_address(all.clone(), "bob@example.org").pinned); + assert_eq!( + by_address(all, "eve@example.org:1962").server, + "example.org:1962" + ); + + store.pin_server("example.org", 1961, &key).unwrap(); + let all = contacts(&store).unwrap(); + assert!(by_address(all.clone(), "bob@example.org").pinned); + assert!( + !by_address(all, "eve@example.org:1962").pinned, + "a default-port pin must not cover another port" + ); + } + #[test] fn pin_lists_under_the_host_port_label() { let store = setup(); diff --git a/src/view.rs b/src/view.rs index 64b0d63..190172f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -11,6 +11,7 @@ use std::sync::atomic::Ordering; use std::time::Duration; +use fumi_core::address::trust_label; use fumi_core::crypto::{b32, fingerprint, KEY_LEN}; use gtk::prelude::*; use gtk::{Align, Orientation, WrapMode}; @@ -20,7 +21,8 @@ use relm4::ComponentSender; use relm4::{Component, ComponentParts}; use crate::app::{ - Carrier, Dialog, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, SignupStep, + Carrier, Dialog, DialogKind, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, + SignupStep, }; use crate::ops::{self, Contact, Folder, Row}; use relm4::RelmApp; @@ -210,6 +212,13 @@ pub struct CarrierWidgets { compose_subject: adw::EntryRow, compose_body: gtk::TextBuffer, compose_send: adw::ButtonContent, + compose_pin_card: gtk::Box, + compose_pin_title: gtk::Label, + compose_pin_body: gtk::Label, + compose_pin_key: gtk::Label, + compose_pin_fingerprint: gtk::Label, + compose_pin_hint: gtk::Label, + compose_pin_button: gtk::Button, // sidebar bottom: own identity and the contact list sidebar_own: gtk::Label, @@ -228,6 +237,7 @@ pub struct CarrierWidgets { public_key_row: adw::ActionRow, rotations_value: gtk::Label, keep_mail_check: gtk::CheckButton, + strict_check: gtk::CheckButton, timeout_row: adw::SpinRow, // onboarding @@ -569,6 +579,56 @@ impl CarrierWidgets { compose_fields.append(&compose_to); compose_fields.append(&compose_subject); compose.append(&compose_fields); + + // Shown when a send was refused because the recipient's key would + // have come from a server with no pin: the refusal and its fix in + // one place, since nowhere else in the app pins a correspondent's + // server. + let compose_pin_title = label("", "heading"); + compose_pin_title.set_wrap(true); + let compose_pin_body = label("", "dim"); + compose_pin_body.set_wrap(true); + let compose_pin_key = label("", "mono"); + compose_pin_key.set_selectable(true); + compose_pin_key.set_wrap(true); + compose_pin_key.set_wrap_mode(gtk::pango::WrapMode::Char); + let compose_pin_fingerprint = label("", "mono dim"); + compose_pin_fingerprint.set_selectable(true); + compose_pin_fingerprint.set_wrap(true); + let compose_pin_hint = label( + "If the server operator published a key or fingerprint, make \ + sure it matches before trusting it.", + "dim caption", + ); + compose_pin_hint.set_wrap(true); + let compose_pin_button = button("", "accent"); + compose_pin_button.set_halign(Align::Start); + compose_pin_button.set_margin_top(6); + let compose_pin_card = gtk::Box::new(Orientation::Vertical, 6); + compose_pin_card.add_css_class("card"); + compose_pin_card.set_visible(false); + for child in [&compose_pin_title, &compose_pin_body] { + child.set_margin_start(12); + child.set_margin_end(12); + } + compose_pin_title.set_margin_top(12); + for child in [ + &compose_pin_key, + &compose_pin_fingerprint, + &compose_pin_hint, + ] { + child.set_margin_start(12); + child.set_margin_end(12); + } + compose_pin_button.set_margin_start(12); + compose_pin_button.set_margin_bottom(12); + compose_pin_card.append(&compose_pin_title); + compose_pin_card.append(&compose_pin_body); + compose_pin_card.append(&compose_pin_key); + compose_pin_card.append(&compose_pin_fingerprint); + compose_pin_card.append(&compose_pin_hint); + compose_pin_card.append(&compose_pin_button); + compose.append(&compose_pin_card); // Frontmatter rows: one `Key: value` entry each, plus a flat // add button below. Rows are rebuilt in render on structural // changes only, so the entries never lose focus while typing. @@ -770,6 +830,32 @@ impl CarrierWidgets { rotations_group.add(&rotate_button); advanced_page.add(&rotations_group); + let trust_group = adw::PreferencesGroup::builder() + .title("First Contact") + .description( + "A recipient who is not a contact yet, and whose address carries \ + no key, has to be looked up on their own server. That answer is \ + only as good as the server: one with no pinned key can return a \ + key of its own and read what you send.", + ) + .build(); + let strict_row = adw::ActionRow::builder() + .title("Require a pinned server") + .subtitle( + "Stop such a send and offer to pin the server's key first, so it \ + can be checked against what the operator published. With this \ + off, the key is taken on trust and the send is only reported as \ + unverified afterwards.", + ) + .subtitle_lines(0) + .activatable(true) + .build(); + let strict_check = gtk::CheckButton::builder().valign(Align::Center).build(); + strict_row.add_suffix(&strict_check); + strict_row.set_activatable_widget(Some(&strict_check)); + trust_group.add(&strict_row); + advanced_page.add(&trust_group); + // The log-out button is wide and destructive, so it sits in the // group's body under the warning rather than squeezed beside the // header where its label would crowd the title. @@ -1086,6 +1172,7 @@ impl CarrierWidgets { add_header.connect_clicked(input(Message::ComposeHeaderAdded)); compose_send_button.connect_clicked(input(Message::SendPressed)); + compose_pin_button.connect_clicked(input(Message::PinServerAction)); compose_cancel.connect_clicked(input(Message::ComposeClosed)); let to_sender = sender.clone(); compose_to.connect_changed(move |entry| { @@ -1123,6 +1210,10 @@ impl CarrierWidgets { keep_mail_check.connect_toggled(move |check| { keep_mail_sender.input(Message::KeepMailChanged(check.is_active())) }); + let strict_sender = sender.clone(); + strict_check.connect_toggled(move |check| { + strict_sender.input(Message::StrictPinningChanged(check.is_active())) + }); // Export/Import go straight to a native file dialog rather than a // Message — the chosen path only matters for this one call, and // round-tripping it through the model would gain nothing. @@ -1238,6 +1329,13 @@ impl CarrierWidgets { compose_subject, compose_body, compose_send, + compose_pin_card, + compose_pin_title, + compose_pin_body, + compose_pin_key, + compose_pin_fingerprint, + compose_pin_hint, + compose_pin_button, sidebar_own, sidebar_contacts: contacts_list, detail_share, @@ -1250,6 +1348,7 @@ impl CarrierWidgets { public_key_row, rotations_value, keep_mail_check, + strict_check, timeout_row, onboard_seed_label, onboard_seed_entry, @@ -1354,8 +1453,8 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { let _ = out.send(OpResult::Generic(res)); }), PendingOp::LearnKey { addr } => sender.spawn_command(move |out| { - let offered = - ops::learn_key(&store, &addr, 30).map(|key| key.map(|key| (addr.clone(), key))); + let offered = ops::learn_key(&store, &addr, timeout) + .map(|key| key.map(|key| (addr.clone(), key))); let _ = out.send(OpResult::PinOffered(offered)); }), PendingOp::Pin { addr, key } => sender.spawn_command(move |out| { @@ -1496,6 +1595,46 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { + w.compose_pin_key.set_text(&b32(&key)); + w.compose_pin_fingerprint.set_text(&fingerprint(&key)); + w.compose_pin_button.set_label("Pin and send"); + } + None => w.compose_pin_button.set_label("Fetch their server's key"), + } + let fetched = prompt.offered.is_some(); + for line in [ + &w.compose_pin_key, + &w.compose_pin_fingerprint, + &w.compose_pin_hint, + ] { + line.set_visible(fetched); + } + } + // Header and sidebar identity. let address = model .snapshot @@ -1642,12 +1781,37 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let block = icon_label_button("action-unavailable-symbolic", "Block", "danger"); @@ -1821,9 +2001,14 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { - let rotating = matches!(dialog, Dialog::Rotate); - let replying = matches!(dialog, Dialog::Reply { .. }); - let resetting = matches!(dialog, Dialog::Reset); + let kind = dialog.kind(); let alert = match dialog { Dialog::Delete { kept, .. } => { let alert = adw::AlertDialog::new( @@ -1974,25 +2160,50 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { + let alert = adw::AlertDialog::new( + Some("Replace this contact's key?"), + Some(&format!( + "{address} is already on file with a different key.\n\nknown {known_fingerprint}\nnew {offered_fingerprint}\n\nThe address you were given carries the new key and would replace the stored one. If this contact did not hand it to you themselves, stop: a key swapped in on the way reads everything you send them." + )), + ); + alert.add_response("cancel", "Cancel"); + alert.add_response("replace", "Replace Key"); + alert.set_response_appearance("replace", adw::ResponseAppearance::Destructive); + alert + } }; // The reply dialog carries its own default: Enter answers the // question instead of cancelling the reply the user asked for. - let close_default = if replying { "without" } else { "cancel" }; + let close_default = if kind == DialogKind::Reply { + "without" + } else { + "cancel" + }; alert.set_close_response("cancel"); alert.set_default_response(Some(close_default)); let response_sender = sender.clone(); alert.connect_response(None, move |_, response| { - response_sender.input(match (rotating, replying, resetting, response) { - (true, _, _, "rotate") => Message::RotateConfirmed, - (true, _, _, _) => Message::RotateCancelled, - (_, true, _, "copy") => Message::ReplyConfirmed(true), - (_, true, _, "without") => Message::ReplyConfirmed(false), - (_, true, _, _) => Message::ReplyCancelled, - (_, _, true, "reset") => Message::ResetConfirmed, - (_, _, true, _) => Message::ResetCancelled, - (false, _, _, "here") => Message::DeleteConfirmed(false), - (false, _, _, "everywhere") => Message::DeleteConfirmed(true), - (false, _, _, _) => Message::DeleteCancelled, + // Anything that is not an explicit confirmation — a + // Cancel, Escape, or the close response — cancels. + response_sender.input(match (kind, response) { + (DialogKind::Delete, "here") => Message::DeleteConfirmed(false), + (DialogKind::Delete, "everywhere") => Message::DeleteConfirmed(true), + (DialogKind::Delete, _) => Message::DeleteCancelled, + (DialogKind::Rotate, "rotate") => Message::RotateConfirmed, + (DialogKind::Rotate, _) => Message::RotateCancelled, + (DialogKind::Reset, "reset") => Message::ResetConfirmed, + (DialogKind::Reset, _) => Message::ResetCancelled, + (DialogKind::Reply, "copy") => Message::ReplyConfirmed(true), + (DialogKind::Reply, "without") => Message::ReplyConfirmed(false), + (DialogKind::Reply, _) => Message::ReplyCancelled, + (DialogKind::KeyChange, "replace") => Message::KeyChangeConfirmed, + (DialogKind::KeyChange, _) => Message::KeyChangeCancelled, }); }); alert.present(Some(&w.window)); From 57e75af3ac7d74e53c037a5663105603fe466fa2 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 22:10:29 +0300 Subject: [PATCH 26/37] feat: extract all user-facing strings into a Fluent catalogue --- Cargo.lock | 153 +++++++++++ Cargo.toml | 2 + locales/en-US/main.ftl | 343 ++++++++++++++++++++++++ src/app.rs | 59 ++--- src/i18n.rs | 64 +++++ src/main.rs | 1 + src/ops.rs | 142 +++++++--- src/view.rs | 586 +++++++++++++++++++++++------------------ 8 files changed, 1024 insertions(+), 326 deletions(-) create mode 100644 locales/en-US/main.ftl create mode 100644 src/i18n.rs diff --git a/Cargo.lock b/Cargo.lock index 6644ddf..411e8bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,7 @@ dependencies = [ name = "carrier" version = "0.1.0" dependencies = [ + "fluent-bundle", "fumi-core", "gtk4", "libadwaita", @@ -125,6 +126,7 @@ dependencies = [ "rand_core", "relm4", "tempfile", + "unic-langid", "winresource", ] @@ -278,6 +280,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -359,6 +372,41 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror", +] + [[package]] name = "flume" version = "0.11.1" @@ -845,6 +893,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1127,6 +1194,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1161,6 +1234,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + [[package]] name = "semver" version = "1.0.28" @@ -1352,6 +1431,37 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + [[package]] name = "tokio" version = "1.53.1" @@ -1443,12 +1553,39 @@ dependencies = [ "once_cell", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.26" @@ -1673,6 +1810,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" + [[package]] name = "zeroize" version = "1.9.0" @@ -1693,6 +1836,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "zerofrom", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 19f960e..6aa56dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ libadwaita = { version = "0.7", features = ["v1_5"] } relm4 = { version = "0.9", features = ["libadwaita"] } rand_core = { version = "0.6", features = ["getrandom"] } qrcodegen = "1" +fluent-bundle = "0.16.0" +unic-langid = "0.9.6" [build-dependencies] winresource = "0.1" diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl new file mode 100644 index 0000000..03a3bc1 --- /dev/null +++ b/locales/en-US/main.ftl @@ -0,0 +1,343 @@ +## Loaded once at startup by `src/i18n.rs`; every user-facing string in +## the app lives here, grouped by the feature it belongs to. + +## Seed parsing — restoring an identity from a pasted seed + +seed-bad-hex = bad hex +seed-wrong-length = a seed is 32 bytes + +## Local files — errors reading/writing the key and its sidecars + +file-error = { $path }: { $reason } +key-file-wrong-length = { $path }: not a 32-byte master + +## Compose — validating a typed `Key: value` header row + +compose-header-colon-missing = a header is `Key: value` — the colon is missing +compose-header-reserved-key = { $key } is attached by the client and cannot be set +compose-header-invalid-key = keys are 1-64 characters of letters, digits and dashes +compose-header-value-multiline = a value must stay on one line +compose-header-duplicate-key = { $key } is set twice +compose-headers-too-large = the headers exceed the frontmatter budget +compose-headers-too-many = too many headers + +## Contacts — importing a smol:// address + +contacts-import-needs-key = import needs a self-certifying smol:// address carrying a key + +## Mail — opening a message by id + +mail-message-id-wrong-length = message ids are 64 hex characters +mail-message-id-wrong-bytes = message ids are 32 bytes +mail-message-not-found = message not found + +## Fetch — the status note after a fetch completes +## +## `$variant` picks the one combination of "N rejected" and "(cancelled)" +## that applies, so each case reads as one complete sentence rather than +## stitched-together fragments. + +fetch-result-note = { $variant -> + [rejected] { $total } fetched, { $stored } stored, { $rejected } rejected + [cancelled] { $total } fetched, { $stored } stored (cancelled) + [rejected-cancelled] { $total } fetched, { $stored } stored, { $rejected } rejected (cancelled) + *[plain] { $total } fetched, { $stored } stored +} + +## Send — the status note after a send completes +## +## `$variant` picks the one combination of trust-change clause and +## accept-token clause that applies, each spelled out as a full sentence. +## Any warning fumi itself reports is appended separately in code, since +## that text is fumi's own and not part of this catalogue. + +send-result-note = { $variant -> + [token] Sent to { $address } ({ $bytes } bytes), accept token attached + [unverified] Sent to { $address } ({ $bytes } bytes); new contact learned from an unpinned server (unverified) + [unverified-token] Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned from an unpinned server (unverified) + [tofu] Sent to { $address } ({ $bytes } bytes); new contact learned (trust on first use) + [tofu-token] Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned (trust on first use) + [rotated] Sent to { $address } ({ $bytes } bytes); contact rotated to a new key (chain verified) + [rotated-token] Sent to { $address } ({ $bytes } bytes), accept token attached; contact rotated to a new key (chain verified) + *[plain] Sent to { $address } ({ $bytes } bytes) +} + +## Background-operation status notes +## +## Built in `spawn_op` (src/view.rs) rather than ops.rs, since that is +## where each `PendingOp` result is assembled for the worker thread. + +status-registered-note = registered +status-restored-note = restored at rotation { $index } +status-deleted-note = deleted +status-accepted-note = accepted { $address } +status-blocked-note = blocked { $address } +status-not-blocked-note = { $address } was not blocked +status-imported-note = imported { $address } (verified) +status-rotated-note = rotated to index { $index }; new key { $key_prefix }… + +## Backup + +backup-written-note = backup written to { $path } + +## Drafts + +draft-saved-note = draft saved + +## Log out + +logged-out-note = logged out. Create a new identity or restore a seed. + +## Onboarding + +identity-created-note = Identity created. Write the seed down; it is the only secret. +seed-restored-note = Seed restored. Bind a username or recall an address. + +## Signup + +signup-enter-host-error = enter the server host first +signup-enter-user-host-error = enter user and host + +## Compose + +compose-enter-recipient-error = enter a recipient +compose-header-row-error = header { $index }: { $reason } +reply-no-address-error = this sender left no address to reply to +reply-subject-prefix = Re: { $subject } +address-invalid-error = that address does not parse + +## Contacts + +contacts-reply-address-invalid-error = the sender's reply address does not parse +contacts-sender-key-invalid-error = the sender's key does not parse +contacts-accept-no-address-error = this sender left no address to accept +contacts-block-no-address-error = this sender left no address to block + +## Common — short words reused verbatim across several buttons/dialogs + +action-cancel = Cancel +action-back = Back +action-block = Block +action-unblock = Unblock +action-accept = Accept +action-reply = Reply +action-delete = Delete +# Shown wherever a fetched server key is on screen before it's pinned: +# the compose's pin card, the signup server step, and the contact detail +# body when a key was fetched for that contact. +trust-fingerprint-hint = If the server operator published a key or fingerprint, make sure it matches before trusting it. + +## Sidebar + +mail-fetch-button = Fetch +mail-write-button = Write +folder-inbox = Inbox +folder-requests = Requests +folder-sent = Sent +folder-drafts = Drafts +settings-nav-label = Settings +identity-you-caption = you +contacts-caption = contacts +contacts-import-placeholder = Import a smol:// address +contacts-import-button = Import + +## Mail — the open-message meta line +## +## `$kept` picks the one variant that applies, each a full sentence, +## rather than an optional clause spliced onto a shared prefix. + +mail-meta-to = to +mail-meta-from = from +mail-meta-line = { $kept -> + *[no] { $label } { $from } · { $time } UTC · id { $id }… + [yes] { $label } { $from } · { $time } UTC · id { $id }… · a server copy still exists +} +mail-no-subject = (no subject) +mail-no-reply-address = no reply address: cannot accept or block +contacts-empty-hint = no contacts + +## Mail list — empty-folder messages, and the own-identity detail + +mail-inbox-empty-title = Inbox is empty +mail-inbox-empty-description = Fetch to check the mailbox. +mail-requests-empty-title = No requests +mail-requests-empty-description = Mail from senders you have not accepted yet arrives here. +mail-sent-empty-title = Nothing sent yet +mail-sent-empty-description = Messages you send keep a sealed copy here. +mail-drafts-empty-title = No drafts +mail-drafts-empty-description = A compose closed from the sidebar is kept here. +own-identity-meta = you · rotation { $rotations } +contacts-pin-button-fetched = Pin this key +contacts-pin-button-unfetched = Pin server + +mail-empty-title = No message selected +mail-empty-description = Pick a message from the list above to read it. +# The two sentences shown under Fetch when the inbox is empty. +mail-empty-hint = Fetching removes mail from the server once it's stored here. + Turn on "Leave mail on server" in Settings to keep it there instead. +detail-qr-tooltip = Show the address as a QR code +detail-copy-tooltip = Copy the address + +## Compose + +compose-to-label = To +compose-subject-label = Subject +compose-add-header-button = + header +compose-send-button = Send +compose-header-placeholder = Key: value +compose-header-remove-tooltip = Remove this header + +## Contacts — trust state shown in the sidebar and the detail pane + +contact-state-accepted = accepted +contact-state-blocked = blocked +contact-state-verified = verified +contact-state-tofu = trust on first use +# `$state` is one of the four words above, already resolved by the caller +# through `contact_state`; this message only adds the server's pin state. +contact-meta-line = { $state } · server { $pinned -> + [yes] pinned + *[no] not pinned +} + +## Detail pane — the monospace key/value block shared by contacts and the +## own identity view. Column widths are padded to these labels in code +## (14 characters), so a much longer translation would throw the +## alignment off; flagged in the extraction report. + +detail-key-label = key +detail-fingerprint-label = fingerprint +detail-server-label = server +contact-server-key-offered = this server presents + +## Settings — tab switcher labels + +settings-tab-identity = Identity +settings-tab-server = Server +settings-tab-backup = Backup +settings-tab-advanced = Advanced + +## Settings — Identity tab + +settings-field-address = Address +settings-field-key = Key +settings-field-fingerprint = Fingerprint +settings-field-uri = URI +property-row-copy-tooltip = Copy the { $field } +settings-recovery-secret-title = Recovery Secret +settings-secret-reveal-tooltip = Reveal the recovery secret +settings-secret-hide-tooltip = Hide the recovery secret +settings-secret-copy-tooltip = Copy the recovery secret + +## Settings — Server tab + +settings-field-host = Host +settings-field-public-key = Public Key +settings-public-key-not-pinned = not pinned yet +server-connection-title = Connection +server-timeout-title = Timeout +server-timeout-subtitle = Seconds before a network call gives up +server-fetching-title = Fetching +server-leave-mail-title = Leave mail on server +server-leave-mail-subtitle = Keep a copy on the server after fetching, so another client can fetch it too. When this is enabled, deleting a message removes it from this client and the server. + +## Settings — Backup tab + +backup-page-description = One file holding your mail, contacts and server info for moving to another client, or for keeping what the server no longer has. It is sealed to this identity and deliberately leaves the secret itself out. +backup-export-title = Export +backup-export-subtitle = Write a backup file +backup-import-title = Import +backup-import-subtitle = Restore from a backup file +backup-export-dialog-title = Export Backup +backup-import-dialog-title = Import Backup + +## Settings — Advanced tab + +advanced-rotations-title = Rotations +advanced-rotations-description = Issues a fresh key from your secret with a signed certificate proving it replaces this one. Contacts accept the change from that chain, and the old key stays readable. Rotation is not revocation. +advanced-rotations-completed = Completed +advanced-rotate-button = Rotate identity +advanced-first-contact-title = First Contact +advanced-first-contact-description = A recipient who is not a contact yet, and whose address carries no key, has to be looked up on their own server. That answer is only as good as the server: one with no pinned key can return a key of its own and read what you send. +advanced-strict-pinning-title = Require a pinned server +advanced-strict-pinning-subtitle = Stop such a send and offer to pin the server's key first, so it can be checked against what the operator published. With this off, the key is taken on trust and the send is only reported as unverified afterwards. +advanced-logout-title = Log Out +advanced-logout-description = Removes this identity from this client entirely: the secret, pins, contacts and every cached message. Without a backup of the secret there is no way back in. Mail already fetched here may have no other copy anywhere, unless "leave mail on server" was on. Whatever is still on the server comes back on the first fetch after logging back in. +advanced-logout-button = Log out and delete all data + +## Onboarding +## +## The app name itself ("carrier") is a brand, not translated prose, and +## is left as a literal in code — see the extraction report. + +onboard-tagline = smolmail: one seed, sealed mail, five operations +onboard-create-button = Create a new identity +onboard-restore-button = Restore existing identity from seed +onboard-created-title = Write this seed down +onboard-created-description = It is the only secret, and it is shown once. +onboard-saved-button = I saved it +onboard-restore-title = Restore from a seed +onboard-seed-entry-title = Seed — 32 bytes, base32 or hex +onboard-restore-go-button = Restore + +## Signup + +signup-server-title = Set up the server +signup-fetch-key-button = Fetch the server's key +signup-continue-button = Continue +signup-start-over-button = Start over +signup-account-title = Choose your address +signup-mode-new-label = Create a new address +signup-mode-existing-label = I already have this address +signup-user-label = User +signup-invite-label = Invite token (if required by the server) +signup-register-button = Register +signup-recall-button = Restore access + +## Compose — the inline pin card shown after a blocked send + +compose-pin-title = { $host } is not pinned +compose-pin-body = { $address } is not a contact yet, so carrier would have to ask this server for their key — and a server with no pinned key can answer with one of its own and read the message. +compose-pin-button-fetch = Fetch their server's key +compose-pin-button-send = Pin and send + +## Dialogs + +delete-dialog-title = Delete this message? +delete-dialog-body-kept = Deleting it here leaves the server's copy in place. Deleting it everywhere asks the server to drop that copy too. +delete-dialog-body-gone = The sealed copy in the local store is removed. +delete-dialog-everywhere-button = Delete Everywhere +rotate-dialog-title = Rotate the identity? +reset-dialog-title = Log out and delete all data? +# `$list` is the sender's own frontmatter rows, already typed by a human +# and reproduced verbatim — not translatable prose itself, just data +# interpolated into this one sentence. +reply-dialog-title = Carry the sender's fields into the reply? +reply-dialog-body = This message carries its own frontmatter:{"\u000A"}{"\u000A"}{ $list }{"\u000A"}{"\u000A"}Reply with copies of these fields, or start with none. The fields stay editable either way. +reply-dialog-cancel-button = Cancel Reply +reply-dialog-without-button = Reply Without +reply-dialog-copy-button = Copy Fields +key-change-dialog-title = Replace this contact's key? +key-change-dialog-body = { $address } is already on file with a different key.{"\u000A"}{"\u000A"}known { $known_fingerprint }{"\u000A"}new { $offered_fingerprint }{"\u000A"}{"\u000A"}The address you were given carries the new key and would replace the stored one. If this contact did not hand it to you themselves, stop: a key swapped in on the way reads everything you send them. +key-change-dialog-replace-button = Replace Key + +## Status bar +## +## `model.busy` also doubles as an internal state-machine key compared +## against literal English in app.rs (e.g. `self.busy.as_deref() == +## Some("fetching")`), so that value itself is not translated — only its +## display here is, via this lookup from the internal label to a phrase. +## See the extraction report for the coupling this implies. + +status-busy-fetching = fetching… +status-busy-refreshing = refreshing… +status-busy-registering = registering… +status-busy-restoring = restoring… +status-fetching-progress = fetching… { $count } in inbox + +## First contact / server trust + +first-contact-blocked-error = { $address } is not a contact yet and { $host } is not pinned — pin their server's key to send. +server-key-offered-note = The server presented this key. Pin it only if the fingerprint matches what the operator published. +server-key-already-pinned-note = This server's key is already pinned. +server-key-pinned-note = server key pinned diff --git a/src/app.rs b/src/app.rs index 9cd9dee..4ccc2d4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,6 +15,7 @@ use fumi_core::crypto::{fingerprint, unb32, KEY_LEN}; use fumi_core::store::Store; use gtk::prelude::*; +use crate::i18n; use crate::ops::{self, Draft, Folder, OpenedMsg, Row, Snapshot}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -514,7 +515,7 @@ impl Carrier { at: fumi_core::store::now(), }); ops::save_drafts(&self.drafts_path(), &self.drafts); - self.note("draft saved".into()); + self.note(i18n::t("draft-saved-note")); } /// Tapping a draft in the folder puts it back in the compose it came @@ -555,7 +556,7 @@ impl Carrier { } let (app, _) = Carrier::boot(self.dir.clone()); *self = app; - self.note("logged out. Create a new identity or restore a seed.".into()); + self.note(i18n::t("logged-out-note")); } /// Cancelling out of a not-yet-registered identity removes the key @@ -853,9 +854,7 @@ impl Carrier { self.master = Some(master); self.onboard.seed_shown = ops::seed_text(&master); self.onboard.mode = OnboardMode::Created; - self.note( - "Identity created. Write the seed down; it is the only secret.".into(), - ); + self.note(i18n::t("identity-created-note")); } } Message::OnboardRestore => { @@ -877,7 +876,7 @@ impl Carrier { } else { self.master = Some(master); self.page = Page::Signup; - self.note("Seed restored. Bind a username or recall an address.".into()); + self.note(i18n::t("seed-restored-note")); } } Err(e) => self.fail(e), @@ -904,14 +903,14 @@ impl Carrier { return Vec::new(); } let Some(addr) = self.signup_host_address() else { - self.fail("enter the server host first".into()); + self.fail(i18n::t("signup-enter-host-error")); return Vec::new(); }; return vec![PendingOp::LearnKey { addr }]; } Message::SignupRegister => { let Some(addr) = self.signup_address() else { - self.fail("enter user and host".into()); + self.fail(i18n::t("signup-enter-user-host-error")); return Vec::new(); }; let invite = if self.signup.invite.is_empty() { @@ -926,7 +925,7 @@ impl Carrier { } Message::SignupRecall => { let Some(addr) = self.signup_address() else { - self.fail("enter user and host".into()); + self.fail(i18n::t("signup-enter-user-host-error")); return Vec::new(); }; if !self.start("restoring") { @@ -993,7 +992,7 @@ impl Carrier { let to = if let Some(uri) = &msg.reply_to { uri.clone() } else if msg.from.starts_with('<') { - self.fail("this sender left no address to reply to".into()); + self.fail(i18n::t("reply-no-address-error")); return Vec::new(); } else { msg.from.clone() @@ -1001,7 +1000,7 @@ impl Carrier { let subject = if msg.subject.is_empty() || msg.subject.starts_with("Re:") { msg.subject.clone() } else { - format!("Re: {}", msg.subject) + i18n::t1("reply-subject-prefix", "subject", msg.subject.clone()) }; // The sender's own fields are the one thing a reply has to // decide about: carry copies, or start clean. Only the @@ -1057,7 +1056,7 @@ impl Carrier { return Vec::new(); }; if compose.to.trim().is_empty() { - self.fail("enter a recipient".into()); + self.fail(i18n::t("compose-enter-recipient-error")); return Vec::new(); } let mut headers = Vec::new(); @@ -1066,7 +1065,10 @@ impl Carrier { Ok(Some(pair)) => headers.push(pair), Ok(None) => {} Err(reason) => { - self.fail(format!("header {}: {reason}", index + 1)); + let mut args = fluent_bundle::FluentArgs::new(); + args.set("index", (index + 1) as i64); + args.set("reason", reason); + self.fail(i18n::t_args("compose-header-row-error", Some(&args))); return Vec::new(); } } @@ -1109,11 +1111,10 @@ impl Carrier { } if self.strict_pinning { if let Some(addr) = self.unpinned_first_contact(&to) { - self.fail(format!( - "{} is not a contact yet and {} is not pinned — pin their server's key to send.", - addr.short(), - trust_label(&addr.host, addr.port), - )); + let mut args = fluent_bundle::FluentArgs::new(); + args.set("address", addr.short()); + args.set("host", trust_label(&addr.host, addr.port)); + self.fail(i18n::t_args("first-contact-blocked-error", Some(&args))); self.pin_prompt = Some(PinPrompt { addr, offered: None, @@ -1139,7 +1140,7 @@ impl Carrier { return Vec::new(); }; let Ok(addr) = Address::parse(&to) else { - self.fail("that address does not parse".into()); + self.fail(i18n::t("address-invalid-error")); return Vec::new(); }; let fetched = self @@ -1203,17 +1204,17 @@ impl Carrier { return Vec::new(); } let Some(uri) = msg.reply_to.clone() else { - self.fail("this sender left no address to accept".into()); + self.fail(i18n::t("contacts-accept-no-address-error")); return Vec::new(); }; let Ok(addr) = Address::parse(&uri) else { - self.fail("the sender's reply address does not parse".into()); + self.fail(i18n::t("contacts-reply-address-invalid-error")); return Vec::new(); }; let Ok(identity) = <[u8; KEY_LEN]>::try_from(unb32(&msg.sender).unwrap_or_default()) else { - self.fail("the sender's key does not parse".into()); + self.fail(i18n::t("contacts-sender-key-invalid-error")); return Vec::new(); }; return vec![PendingOp::Accept { @@ -1226,11 +1227,11 @@ impl Carrier { return Vec::new(); }; let Some(uri) = msg.reply_to.clone() else { - self.fail("this sender left no address to block".into()); + self.fail(i18n::t("contacts-block-no-address-error")); return Vec::new(); }; let Ok(addr) = Address::parse(&uri) else { - self.fail("the sender's reply address does not parse".into()); + self.fail(i18n::t("contacts-reply-address-invalid-error")); return Vec::new(); }; return vec![PendingOp::Block { @@ -1414,11 +1415,7 @@ impl Carrier { Some(prompt) => prompt.offered = Some(key), None => self.pin_offer = Some((addr, key)), } - self.note( - "The server presented this key. Pin it only if the fingerprint \ - matches what the operator published." - .into(), - ); + self.note(i18n::t("server-key-offered-note")); None } OpResult::PinOffered(Ok(None)) => { @@ -1428,7 +1425,7 @@ impl Carrier { if self.pin_prompt.take().is_none() { self.signup.key_trusted = true; } - self.note("This server's key is already pinned.".into()); + self.note(i18n::t("server-key-already-pinned-note")); None } OpResult::PinOffered(Err(e)) => { @@ -1438,7 +1435,7 @@ impl Carrier { } OpResult::Pinned(Ok(())) => { self.done(); - self.note("server key pinned".into()); + self.note(i18n::t("server-key-pinned-note")); // The contact detail shows the pin state, so that pane // needs the fresh row; signup's own pin changes nothing // that is on screen. diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..bda0fbe --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,64 @@ +//! A single Fluent bundle, loaded once from `locales/en-US/main.ftl` and +//! baked into the binary via `include_str!`. Every user-facing string in +//! the app is looked up through `t`/`t1`/`t_args` rather than written +//! inline, so copy review and translation both have one place to work +//! from. There is only one locale today; a future one would plug in here +//! without touching a call site. + +use std::sync::OnceLock; + +use fluent_bundle::concurrent::FluentBundle; +use fluent_bundle::{FluentArgs, FluentResource, FluentValue}; + +fn bundle() -> &'static FluentBundle { + static BUNDLE: OnceLock> = OnceLock::new(); + BUNDLE.get_or_init(|| { + let ftl = include_str!("../locales/en-US/main.ftl"); + let resource = + FluentResource::try_new(ftl.to_string()).expect("main.ftl must parse as Fluent"); + let locale = "en-US".parse().expect("\"en-US\" is a valid language tag"); + let mut bundle = FluentBundle::new_concurrent(vec![locale]); + // Bidi isolation marks are invisible but real characters; with a + // single left-to-right locale they only cost correctness — + // copy/paste, clipboard round-trips and plain string equality all + // pick them up — for no visible benefit. + bundle.set_use_isolating(false); + bundle + .add_resource(resource) + .expect("main.ftl must not redefine a message id"); + bundle + }) +} + +/// Looks up a message with no variables. +pub fn t(id: &str) -> String { + t_args(id, None) +} + +/// Looks up a message with one variable — the common case, since most +/// interpolated strings here carry a single value. +pub fn t1(id: &str, key: &'static str, value: impl Into>) -> String { + let mut args = FluentArgs::new(); + args.set(key, value); + t_args(id, Some(&args)) +} + +/// Looks up a message against a caller-built argument set, for the rare +/// string that interpolates more than one value. +pub fn t_args(id: &str, args: Option<&FluentArgs>) -> String { + let bundle = bundle(); + // A missing id or a value-less term is a copy bug, not a runtime + // condition to hide: surface it in the string itself rather than + // silently falling back to English prose that isn't there to fall + // back to (there's only the one locale). + let Some(message) = bundle.get_message(id) else { + return format!("???{id}???"); + }; + let Some(pattern) = message.value() else { + return format!("???{id}???"); + }; + let mut errors = Vec::new(); + bundle + .format_pattern(pattern, args, &mut errors) + .into_owned() +} diff --git a/src/main.rs b/src/main.rs index a2c2c78..6b112e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod app; #[cfg(debug_assertions)] mod demo; +mod i18n; mod ops; mod view; diff --git a/src/ops.rs b/src/ops.rs index 7764681..5226335 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -15,6 +15,8 @@ use fumi_core::message::{FRONTMATTER_KEYS, FRONTMATTER_MAX}; use fumi_core::store::Store; use fumi_core::{account, client, export}; +use crate::i18n; + /// One message in a folder list, already opened for its metadata. #[derive(Debug, Clone)] pub struct Row { @@ -316,6 +318,19 @@ fn explain(e: Error) -> String { e.to_string() } +/// `{ $path }: { $reason }` — the one shared shape every local file error +/// here takes; `reason` is whatever the OS says, which this catalogue +/// cannot translate, so only the separator around it is localized. +fn file_error_args( + path: &Path, + reason: &dyn std::fmt::Display, +) -> fluent_bundle::FluentArgs<'static> { + let mut args = fluent_bundle::FluentArgs::new(); + args.set("path", path.display().to_string()); + args.set("reason", reason.to_string()); + args +} + pub fn new_master() -> [u8; KEY_LEN] { use rand_core::RngCore; let mut master = [0u8; KEY_LEN]; @@ -333,23 +348,26 @@ pub fn parse_seed(text: &str) -> Result<[u8; KEY_LEN], String> { let bytes = if text.len() == 64 && text.chars().all(|c| c.is_ascii_hexdigit()) { let mut raw = Vec::with_capacity(32); for pair in text.as_bytes().chunks(2) { - let hi = (pair[0] as char).to_digit(16).ok_or("bad hex")?; - let lo = (pair[1] as char).to_digit(16).ok_or("bad hex")?; + let hi = (pair[0] as char) + .to_digit(16) + .ok_or_else(|| i18n::t("seed-bad-hex"))?; + let lo = (pair[1] as char) + .to_digit(16) + .ok_or_else(|| i18n::t("seed-bad-hex"))?; raw.push((hi * 16 + lo) as u8); } raw } else { unb32(text).map_err(|e| explain(e))? }; - bytes - .try_into() - .map_err(|_| "a seed is 32 bytes".to_string()) + bytes.try_into().map_err(|_| i18n::t("seed-wrong-length")) } pub fn load_key(path: &Path) -> Result<[u8; KEY_LEN], String> { - let raw = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + let raw = std::fs::read(path) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(path, &e))))?; raw.try_into() - .map_err(|_| format!("{}: not a 32-byte master", path.display())) + .map_err(|_| i18n::t1("key-file-wrong-length", "path", path.display().to_string())) } pub fn save_key(path: &Path, master: &[u8; KEY_LEN]) -> Result<(), String> { @@ -377,7 +395,7 @@ pub fn delete_key(path: &Path) -> Result<(), String> { match std::fs::remove_file(path) { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(format!("{}: {e}", path.display())), + Err(e) => Err(i18n::t_args("file-error", Some(&file_error_args(path, &e)))), } } @@ -397,7 +415,10 @@ pub fn factory_reset(dir: &Path) -> Result<(), String> { let path = dir.join(name); if let Err(e) = std::fs::remove_file(&path) { if e.kind() != std::io::ErrorKind::NotFound { - return Err(format!("{}: {e}", path.display())); + return Err(i18n::t_args( + "file-error", + Some(&file_error_args(&path, &e)), + )); } } } @@ -481,14 +502,18 @@ pub fn fetch( cancel: Some(&cancel), }; let fetched = client::fetch_with(store, &me, opts).map_err(explain)?; - let mut note = format!("{} fetched, {} stored", fetched.total, fetched.stored); - if !fetched.rejected.is_empty() { - note.push_str(&format!(", {} rejected", fetched.rejected.len())); - } - if fetched.cancelled { - note.push_str(" (cancelled)"); - } - Ok(note) + let variant = match (!fetched.rejected.is_empty(), fetched.cancelled) { + (true, true) => "rejected-cancelled", + (true, false) => "rejected", + (false, true) => "cancelled", + (false, false) => "plain", + }; + let mut args = fluent_bundle::FluentArgs::new(); + args.set("variant", variant); + args.set("total", fetched.total as i64); + args.set("stored", fetched.stored as i64); + args.set("rejected", fetched.rejected.len() as i64); + Ok(i18n::t_args("fetch-result-note", Some(&args))) } /// Fields fumi itself attaches to every message (sec 5.7, 5.8); a @@ -506,14 +531,18 @@ pub fn header_row(text: &str) -> Result, String> { return Ok(None); } let Some((key, value)) = text.split_once(':') else { - return Err("a header is `Key: value` — the colon is missing".into()); + return Err(i18n::t("compose-header-colon-missing")); }; let key = key.trim(); if RESERVED_HEADERS .iter() .any(|reserved| key.eq_ignore_ascii_case(reserved)) { - return Err(format!("{key} is attached by the client and cannot be set")); + return Err(i18n::t1( + "compose-header-reserved-key", + "key", + key.to_string(), + )); } if key.is_empty() || key.len() > 64 @@ -521,10 +550,10 @@ pub fn header_row(text: &str) -> Result, String> { .bytes() .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') { - return Err("keys are 1-64 characters of letters, digits and dashes".into()); + return Err(i18n::t("compose-header-invalid-key")); } if value.contains('\n') || value.contains('\r') { - return Err("a value must stay on one line".into()); + return Err(i18n::t("compose-header-value-multiline")); } Ok(Some((key.to_string(), value.trim().to_string()))) } @@ -536,7 +565,7 @@ pub fn validate_headers(headers: &[(String, String)]) -> Result<(), String> { let mut seen: std::collections::HashSet = std::collections::HashSet::new(); for (key, _) in headers { if !seen.insert(key.to_ascii_lowercase()) { - return Err(format!("{key} is set twice")); + return Err(i18n::t1("compose-header-duplicate-key", "key", key.clone())); } } let budget: usize = headers @@ -546,10 +575,10 @@ pub fn validate_headers(headers: &[(String, String)]) -> Result<(), String> { // The client adds Subject, In-Reply-To, a Reply-To URI and possibly an // Accept token on top of the user's rows; 512 bytes covers them all. if budget + 512 > FRONTMATTER_MAX { - return Err("the headers exceed the frontmatter budget".into()); + return Err(i18n::t("compose-headers-too-large")); } if headers.len() + 4 > FRONTMATTER_KEYS { - return Err("too many headers".into()); + return Err(i18n::t("compose-headers-too-many")); } Ok(()) } @@ -592,20 +621,27 @@ pub fn send( no_pad: false, }; let sent = client::send(store, &me, &draft, timeout).map_err(explain)?; - let change = match sent.change { - TrustChange::New { unverified: true } => { - "; new contact learned from an unpinned server (unverified)" - } - TrustChange::New { unverified: false } => "; new contact learned (trust on first use)", - TrustChange::Rotated => "; contact rotated to a new key (chain verified)", - TrustChange::None => "", + let trust = match sent.change { + TrustChange::New { unverified: true } => Some("unverified"), + TrustChange::New { unverified: false } => Some("tofu"), + TrustChange::Rotated => Some("rotated"), + TrustChange::None => None, }; - let mut note = format!("sent to {} ({} bytes)", addr.short(), sent.bytes); - if sent.token_used { - note.push_str(", accept token attached"); - } - note.push_str(change); + let variant = match (trust, sent.token_used) { + (Some(trust), true) => format!("{trust}-token"), + (Some(trust), false) => trust.to_string(), + (None, true) => "token".to_string(), + (None, false) => "plain".to_string(), + }; + let mut args = fluent_bundle::FluentArgs::new(); + args.set("variant", variant); + args.set("address", addr.short()); + args.set("bytes", sent.bytes as i64); + let mut note = i18n::t_args("send-result-note", Some(&args)); if let Some(warning) = sent.warning { + // Fumi's own warning text, appended as a second, independent + // clause rather than folded into the catalogue — it isn't ours + // to translate. note.push_str(&format!("; {warning}")); } Ok(note) @@ -669,7 +705,7 @@ pub fn import_contact(store: &Store, uri: &str) -> Result<(String, String), Stri let addr = parse_address(uri)?; let key = addr .identity - .ok_or("import needs a self-certifying smol:// address carrying a key")?; + .ok_or_else(|| i18n::t("contacts-import-needs-key"))?; store .save_contact(&addr.short(), &key, true) .map_err(explain)?; @@ -723,8 +759,13 @@ pub fn rotate( /// Writes the gsmol backup to the chosen path. pub fn export_backup(store: &Store, master: &[u8; KEY_LEN], path: &Path) -> Result { let text = export::export(store, master).map_err(explain)?; - std::fs::write(path, &text).map_err(|e| format!("{}: {e}", path.display()))?; - Ok(format!("backup written to {}", path.display())) + std::fs::write(path, &text) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(path, &e))))?; + Ok(i18n::t1( + "backup-written-note", + "path", + path.display().to_string(), + )) } pub fn import_backup(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result { @@ -739,10 +780,10 @@ fn hex32(text: &str) -> Result<[u8; KEY_LEN], String> { .collect::, _>>() .map_err(|e| e.to_string())? } else { - return Err("message ids are 64 hex characters".to_string()); + return Err(i18n::t("mail-message-id-wrong-length")); }; raw.try_into() - .map_err(|_| "message ids are 32 bytes".to_string()) + .map_err(|_| i18n::t("mail-message-id-wrong-bytes")) } /// Lists one folder as rows, marking unread against the local read set. @@ -796,7 +837,7 @@ pub fn open( .map_err(explain)? .into_iter() .find(|m| m.id == raw) - .ok_or_else(|| "message not found".to_string())?; + .ok_or_else(|| i18n::t("mail-message-not-found"))?; let described = client::describe(store, &me, &stored).map_err(explain)?; // The subject sits in the header, the reply address powers Reply and // the accept/block actions, and the Accept token is consumed by the @@ -1064,6 +1105,23 @@ mod tests { Store::open_in_memory().expect("in-memory store") } + /// Fluent wraps every interpolated value in invisible bidi-isolation + /// marks by default; disabled in `i18n::bundle()` because this app + /// has one left-to-right locale and those marks would otherwise leak + /// into copy/paste, the clipboard and plain string comparisons. + #[test] + fn interpolated_messages_carry_no_bidi_isolation_marks() { + let mut args = fluent_bundle::FluentArgs::new(); + args.set("variant", "plain"); + args.set("total", 5i64); + args.set("stored", 5i64); + args.set("rejected", 0i64); + assert_eq!( + i18n::t_args("fetch-result-note", Some(&args)), + "5 fetched, 5 stored" + ); + } + #[test] fn seed_round_trips_through_base32_and_hex() { let m = master(); diff --git a/src/view.rs b/src/view.rs index 190172f..3bddc89 100644 --- a/src/view.rs +++ b/src/view.rs @@ -24,6 +24,7 @@ use crate::app::{ Carrier, Dialog, DialogKind, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, SignupStep, }; +use crate::i18n; use crate::ops::{self, Contact, Folder, Row}; use relm4::RelmApp; @@ -167,13 +168,14 @@ fn buffer_text(buffer: >k::TextBuffer) -> String { /// The contact's trust state as one word, the same vocabulary the sidebar, /// the detail pane and the contacts page all label it with. -fn contact_state(contact: &Contact) -> &'static str { - match contact.accepted { - Some(true) => "accepted", - Some(false) => "blocked", - None if contact.verified => "verified", - None => "trust on first use", - } +fn contact_state(contact: &Contact) -> String { + let id = match contact.accepted { + Some(true) => "contact-state-accepted", + Some(false) => "contact-state-blocked", + None if contact.verified => "contact-state-verified", + None => "contact-state-tofu", + }; + i18n::t(id) } #[derive(Debug, Clone)] @@ -315,8 +317,16 @@ impl CarrierWidgets { // leave neither of them reading as the primary one. let sidebar_actions = gtk::Box::new(Orientation::Horizontal, 6); sidebar_actions.set_homogeneous(true); - let fetch_button = icon_label_button("mail-send-receive-symbolic", "Fetch", "accent"); - let write_button = icon_label_button("mail-message-new-symbolic", "Write", ""); + let fetch_button = icon_label_button( + "mail-send-receive-symbolic", + &i18n::t("mail-fetch-button"), + "accent", + ); + let write_button = icon_label_button( + "mail-message-new-symbolic", + &i18n::t("mail-write-button"), + "", + ); sidebar_actions.append(&fetch_button); sidebar_actions.append(&write_button); sidebar_top.append(&sidebar_actions); @@ -327,10 +337,10 @@ impl CarrierWidgets { folders.add_css_class("navigation-sidebar"); folders.set_selection_mode(gtk::SelectionMode::Single); let folder_rows = [ - nav_row("folder-download-symbolic", "Inbox"), - nav_row("contact-new-symbolic", "Requests"), - nav_row("mail-send-symbolic", "Sent"), - nav_row("document-edit-symbolic", "Drafts"), + nav_row("folder-download-symbolic", &i18n::t("folder-inbox")), + nav_row("contact-new-symbolic", &i18n::t("folder-requests")), + nav_row("mail-send-symbolic", &i18n::t("folder-sent")), + nav_row("document-edit-symbolic", &i18n::t("folder-drafts")), ]; let mut folder_counts = Vec::new(); let mut drafts_nav = None; @@ -357,7 +367,10 @@ impl CarrierWidgets { let settings_nav = gtk::ListBox::new(); settings_nav.add_css_class("navigation-sidebar"); settings_nav.set_selection_mode(gtk::SelectionMode::Single); - let (settings_row, _) = nav_row("preferences-system-symbolic", "Settings"); + let (settings_row, _) = nav_row( + "preferences-system-symbolic", + &i18n::t("settings-nav-label"), + ); settings_nav.append(&settings_row); sidebar_top.append(&settings_nav); @@ -369,7 +382,7 @@ impl CarrierWidgets { own_text.set_margin_top(8); own_text.set_margin_bottom(8); own_text.set_margin_start(12); - let own_caption = label("you", "caption"); + let own_caption = label(&i18n::t("identity-you-caption"), "caption"); let sidebar_own = label("", "mono heading"); // Not selectable: a selectable label eats clicks on the text with // its selection gesture, and the address is already copyable from @@ -387,14 +400,18 @@ impl CarrierWidgets { // The list's header carries its one action: importing a // smol:// address, in a popover off a plain add button. let contacts_head = gtk::Box::new(Orientation::Horizontal, 6); - let contacts_caption = label("contacts", "caption"); + let contacts_caption = label(&i18n::t("contacts-caption"), "caption"); contacts_caption.set_hexpand(true); contacts_caption.set_valign(Align::Center); contacts_caption.set_margin_start(12); let contact_import = adw::EntryRow::builder() - .title("Import a smol:// address") + .title(i18n::t("contacts-import-placeholder")) .build(); - let import_button = icon_label_button("document-open-symbolic", "Import", "accent"); + let import_button = icon_label_button( + "document-open-symbolic", + &i18n::t("contacts-import-button"), + "accent", + ); import_button.set_valign(Align::Center); contact_import.add_suffix(&import_button); let import_list = boxed_list(); @@ -410,7 +427,7 @@ impl CarrierWidgets { add_contact.set_icon_name("list-add-symbolic"); add_contact.add_css_class("flat"); add_contact.add_css_class("circular"); - add_contact.set_tooltip_text(Some("Import a smol:// address")); + add_contact.set_tooltip_text(Some(&i18n::t("contacts-import-placeholder"))); add_contact.set_popover(Some(&import_popover)); contacts_head.append(&contacts_caption); contacts_head.append(&add_contact); @@ -438,13 +455,13 @@ impl CarrierWidgets { // An empty folder gets a status page rather than a stray line of // dim text, so the pane says what to do about it. let list_empty = adw::StatusPage::new(); - let list_empty_fetch = icon_label_button("mail-send-receive-symbolic", "Fetch", "pill"); - list_empty_fetch.set_halign(Align::Center); - let list_empty_hint = label( - "Fetching removes mail from the server once it's stored here.\n\ - Turn on \"Keep mail on the server\" in Settings to keep it there instead.", - "dim caption", + let list_empty_fetch = icon_label_button( + "mail-send-receive-symbolic", + &i18n::t("mail-fetch-button"), + "pill", ); + list_empty_fetch.set_halign(Align::Center); + let list_empty_hint = label(&i18n::t("mail-empty-hint"), "dim caption"); list_empty_hint.set_wrap(true); list_empty_hint.set_justify(gtk::Justification::Center); list_empty_hint.set_halign(Align::Center); @@ -484,13 +501,13 @@ impl CarrierWidgets { detail_share.set_icon_name("send-to-symbolic"); detail_share.add_css_class("flat"); detail_share.add_css_class("circular"); - detail_share.set_tooltip_text(Some("Show the address as a QR code")); + detail_share.set_tooltip_text(Some(&i18n::t("detail-qr-tooltip"))); detail_share.set_popover(Some(&detail_popover)); detail_share.set_valign(Align::Start); detail_share.set_visible(false); // Copies the address the header shows, so it serves the contact // and the own-identity detail alike. - let detail_copy = icon_button("edit-copy-symbolic", "Copy the address"); + let detail_copy = icon_button("edit-copy-symbolic", &i18n::t("detail-copy-tooltip")); detail_copy.set_valign(Align::Start); detail_copy.set_visible(false); let copy_source = article_subject.clone(); @@ -542,8 +559,8 @@ impl CarrierWidgets { // No icon: the pane sits directly under the list's own empty page, // and two envelopes stacked up the window say nothing twice. let article_empty = adw::StatusPage::builder() - .title("No message selected") - .description("Pick a message from the list above to read it.") + .title(i18n::t("mail-empty-title")) + .description(i18n::t("mail-empty-description")) .build(); let article_stack = gtk::Stack::new(); article_stack.set_hhomogeneous(false); @@ -574,8 +591,12 @@ impl CarrierWidgets { compose.set_margin_start(12); compose.set_margin_end(12); let compose_fields = boxed_list(); - let compose_to = adw::EntryRow::builder().title("To").build(); - let compose_subject = adw::EntryRow::builder().title("Subject").build(); + let compose_to = adw::EntryRow::builder() + .title(i18n::t("compose-to-label")) + .build(); + let compose_subject = adw::EntryRow::builder() + .title(i18n::t("compose-subject-label")) + .build(); compose_fields.append(&compose_to); compose_fields.append(&compose_subject); compose.append(&compose_fields); @@ -595,11 +616,7 @@ impl CarrierWidgets { let compose_pin_fingerprint = label("", "mono dim"); compose_pin_fingerprint.set_selectable(true); compose_pin_fingerprint.set_wrap(true); - let compose_pin_hint = label( - "If the server operator published a key or fingerprint, make \ - sure it matches before trusting it.", - "dim caption", - ); + let compose_pin_hint = label(&i18n::t("trust-fingerprint-hint"), "dim caption"); compose_pin_hint.set_wrap(true); let compose_pin_button = button("", "accent"); compose_pin_button.set_halign(Align::Start); @@ -634,7 +651,7 @@ impl CarrierWidgets { // changes only, so the entries never lose focus while typing. let compose_headers = boxed_list(); compose.append(&compose_headers); - let add_header = button("+ header", "quiet"); + let add_header = button(&i18n::t("compose-add-header-button"), "quiet"); add_header.set_halign(Align::Start); compose.append(&add_header); let compose_view = gtk::TextView::new(); @@ -652,10 +669,10 @@ impl CarrierWidgets { let compose_send_button = button("", "accent"); let compose_send = adw::ButtonContent::builder() .icon_name("mail-send-symbolic") - .label("Send") + .label(i18n::t("compose-send-button")) .build(); compose_send_button.set_child(Some(&compose_send)); - let compose_cancel = button("Cancel", "quiet"); + let compose_cancel = button(&i18n::t("action-cancel"), "quiet"); let compose_actions = gtk::Box::new(Orientation::Horizontal, 6); compose_actions.set_halign(Align::End); compose_actions.append(&compose_cancel); @@ -673,9 +690,13 @@ impl CarrierWidgets { // itself room to wrap instead of being ellipsized into a suffix. // Markup is off so the copy button can hand back exactly what is // on screen. - let property_row = |title: &str| -> adw::ActionRow { + // `id` is a message id (e.g. "settings-field-host"), not literal + // text, so both the title and its "Copy the …" tooltip come from + // the catalogue rather than one being built from the other. + let property_row = |id: &str| -> adw::ActionRow { + let title = i18n::t(id); let row = adw::ActionRow::builder() - .title(title) + .title(&title) .use_markup(false) .subtitle_lines(0) .subtitle_selectable(true) @@ -684,7 +705,7 @@ impl CarrierWidgets { row.add_css_class("monospace"); let copy = icon_button( "edit-copy-symbolic", - &format!("Copy the {}", title.to_lowercase()), + &i18n::t1("property-row-copy-tooltip", "field", title.to_lowercase()), ); let copy_source = row.clone(); copy.connect_clicked(move |_| { @@ -704,8 +725,13 @@ impl CarrierWidgets { let identity_page = adw::PreferencesPage::new(); let identity_group = adw::PreferencesGroup::new(); let mut identity_rows = Vec::new(); - for title in ["Address", "Key", "Fingerprint", "URI"] { - let row = property_row(title); + for field in [ + "settings-field-address", + "settings-field-key", + "settings-field-fingerprint", + "settings-field-uri", + ] { + let row = property_row(field); identity_group.add(&row); identity_rows.push(row); } @@ -714,7 +740,7 @@ impl CarrierWidgets { // The group header names this one, so the row carries only the // secret — masked until the reveal button is pressed. let secret_group = adw::PreferencesGroup::builder() - .title("Recovery Secret") + .title(i18n::t("settings-recovery-secret-title")) .build(); let secret_row = adw::ActionRow::builder() .use_markup(false) @@ -722,8 +748,14 @@ impl CarrierWidgets { .title_selectable(true) .build(); secret_row.add_css_class("monospace"); - let secret_reveal = icon_button("view-reveal-symbolic", "Reveal the recovery secret"); - let secret_copy = icon_button("edit-copy-symbolic", "Copy the recovery secret"); + let secret_reveal = icon_button( + "view-reveal-symbolic", + &i18n::t("settings-secret-reveal-tooltip"), + ); + let secret_copy = icon_button( + "edit-copy-symbolic", + &i18n::t("settings-secret-copy-tooltip"), + ); secret_row.add_suffix(&secret_reveal); secret_row.add_suffix(&secret_copy); secret_group.add(&secret_row); @@ -731,31 +763,31 @@ impl CarrierWidgets { settings_stack.add_titled_with_icon( &identity_page, Some("identity"), - "Identity", + &i18n::t("settings-tab-identity"), "avatar-default-symbolic", ); // -- Server tab ---------------------------------------------------- let server_page = adw::PreferencesPage::new(); - let server_group = adw::PreferencesGroup::builder().title("Connection").build(); - let host_row = property_row("Host"); - let public_key_row = property_row("Public Key"); + let server_group = adw::PreferencesGroup::builder() + .title(i18n::t("server-connection-title")) + .build(); + let host_row = property_row("settings-field-host"); + let public_key_row = property_row("settings-field-public-key"); server_group.add(&host_row); server_group.add(&public_key_row); let timeout_row = adw::SpinRow::with_range(1.0, 600.0, 1.0); - timeout_row.set_title("Timeout"); - timeout_row.set_subtitle("Seconds before a network call gives up"); + timeout_row.set_title(&i18n::t("server-timeout-title")); + timeout_row.set_subtitle(&i18n::t("server-timeout-subtitle")); server_group.add(&timeout_row); server_page.add(&server_group); - let network_group = adw::PreferencesGroup::builder().title("Fetching").build(); + let network_group = adw::PreferencesGroup::builder() + .title(i18n::t("server-fetching-title")) + .build(); let keep_mail_row = adw::ActionRow::builder() - .title("Leave mail on server") - .subtitle( - "Keep a copy on the server after fetching, so another client can \ - fetch it too. When this is enabled, deleting a message removes \ - it from this client and the server.", - ) + .title(i18n::t("server-leave-mail-title")) + .subtitle(i18n::t("server-leave-mail-subtitle")) .subtitle_lines(0) .activatable(true) .build(); @@ -767,30 +799,25 @@ impl CarrierWidgets { settings_stack.add_titled_with_icon( &server_page, Some("server"), - "Server", + &i18n::t("settings-tab-server"), "network-server-symbolic", ); // -- Backup tab ---------------------------------------------------- let backup_page = adw::PreferencesPage::new(); let backup_group = adw::PreferencesGroup::builder() - .description( - "One file holding your mail, contacts and server info for moving \ - to another client, or for keeping what the server no longer \ - has. It is sealed to this identity and deliberately leaves the \ - secret itself out.", - ) + .description(i18n::t("backup-page-description")) .build(); let export_row = adw::ActionRow::builder() - .title("Export") - .subtitle("Write a backup file") + .title(i18n::t("backup-export-title")) + .subtitle(i18n::t("backup-export-subtitle")) .activatable(true) .build(); export_row.add_prefix(>k::Image::from_icon_name("document-save-symbolic")); export_row.add_suffix(>k::Image::from_icon_name("go-next-symbolic")); let import_row = adw::ActionRow::builder() - .title("Import") - .subtitle("Restore from a backup file") + .title(i18n::t("backup-import-title")) + .subtitle(i18n::t("backup-import-subtitle")) .activatable(true) .build(); import_row.add_prefix(>k::Image::from_icon_name("document-open-symbolic")); @@ -801,52 +828,43 @@ impl CarrierWidgets { settings_stack.add_titled_with_icon( &backup_page, Some("backup"), - "Backup", + &i18n::t("settings-tab-backup"), "document-save-symbolic", ); // -- Advanced tab -------------------------------------------------- let advanced_page = adw::PreferencesPage::new(); let rotations_group = adw::PreferencesGroup::builder() - .title("Rotations") - .description( - "Issues a fresh key from your secret with a signed certificate \ - proving it replaces this one. Contacts accept the change from \ - that chain, and the old key stays readable. Rotation is not \ - revocation.", - ) + .title(i18n::t("advanced-rotations-title")) + .description(i18n::t("advanced-rotations-description")) .build(); // Both actions sit in their group's body, centred under the // explanation: a long description wraps under the group title and // would run straight into a header-suffix button. - let rotations_row = adw::ActionRow::builder().title("Completed").build(); + let rotations_row = adw::ActionRow::builder() + .title(i18n::t("advanced-rotations-completed")) + .build(); let rotations_value = label("", "dim numeric"); rotations_value.set_valign(Align::Center); rotations_row.add_suffix(&rotations_value); rotations_group.add(&rotations_row); - let rotate_button = icon_label_button("view-refresh-symbolic", "Rotate identity", ""); + let rotate_button = icon_label_button( + "view-refresh-symbolic", + &i18n::t("advanced-rotate-button"), + "", + ); rotate_button.set_halign(Align::Center); rotate_button.set_margin_top(12); rotations_group.add(&rotate_button); advanced_page.add(&rotations_group); let trust_group = adw::PreferencesGroup::builder() - .title("First Contact") - .description( - "A recipient who is not a contact yet, and whose address carries \ - no key, has to be looked up on their own server. That answer is \ - only as good as the server: one with no pinned key can return a \ - key of its own and read what you send.", - ) + .title(i18n::t("advanced-first-contact-title")) + .description(i18n::t("advanced-first-contact-description")) .build(); let strict_row = adw::ActionRow::builder() - .title("Require a pinned server") - .subtitle( - "Stop such a send and offer to pin the server's key first, so it \ - can be checked against what the operator published. With this \ - off, the key is taken on trust and the send is only reported as \ - unverified afterwards.", - ) + .title(i18n::t("advanced-strict-pinning-title")) + .subtitle(i18n::t("advanced-strict-pinning-subtitle")) .subtitle_lines(0) .activatable(true) .build(); @@ -860,19 +878,12 @@ impl CarrierWidgets { // group's body under the warning rather than squeezed beside the // header where its label would crowd the title. let logout_group = adw::PreferencesGroup::builder() - .title("Log Out") - .description( - "Removes this identity from this client entirely: the secret, \ - pins, contacts and every cached message. Without a backup of \ - the secret there is no way back in. Mail already fetched here \ - may have no other copy anywhere, unless \"leave mail on \ - server\" was on. Whatever is still on the server comes back \ - on the first fetch after logging back in.", - ) + .title(i18n::t("advanced-logout-title")) + .description(i18n::t("advanced-logout-description")) .build(); let reset_button = icon_label_button( "system-log-out-symbolic", - "Log out and delete all data", + &i18n::t("advanced-logout-button"), "danger", ); reset_button.set_halign(Align::Center); @@ -882,7 +893,7 @@ impl CarrierWidgets { settings_stack.add_titled_with_icon( &advanced_page, Some("advanced"), - "Advanced", + &i18n::t("settings-tab-advanced"), "emblem-system-symbolic", ); @@ -918,23 +929,20 @@ impl CarrierWidgets { let title = label("carrier", "title"); title.set_halign(Align::Center); onboard_start.append(&title); - let tagline = label("smolmail: one seed, sealed mail, five operations", "dim"); + let tagline = label(&i18n::t("onboard-tagline"), "dim"); tagline.set_halign(Align::Center); tagline.set_margin_bottom(12); onboard_start.append(&tagline); - let create_button = button("Create a new identity", "pill"); - let restore_button = button("Restore existing identity from seed", "pill-quiet"); + let create_button = button(&i18n::t("onboard-create-button"), "pill"); + let restore_button = button(&i18n::t("onboard-restore-button"), "pill-quiet"); onboard_start.append(&create_button); onboard_start.append(&restore_button); stack.add_named(¢er(&onboard_start), Some("onboard")); let onboard_created = gtk::Box::new(Orientation::Vertical, 12); - let created_title = label("Write this seed down", "subtitle"); + let created_title = label(&i18n::t("onboard-created-title"), "subtitle"); onboard_created.append(&created_title); - onboard_created.append(&label( - "It is the only secret, and it is shown once.", - "dim", - )); + onboard_created.append(&label(&i18n::t("onboard-created-description"), "dim")); let onboard_seed_label = label("", "mono subtitle"); onboard_seed_label.set_selectable(true); onboard_seed_label.set_wrap(true); @@ -948,31 +956,33 @@ impl CarrierWidgets { seed_card.add_css_class("card"); seed_card.append(&onboard_seed_label); onboard_created.append(&seed_card); - let saved_button = button("I saved it", "pill"); - let created_back = button("Back", "pill-quiet"); + let saved_button = button(&i18n::t("onboard-saved-button"), "pill"); + let created_back = button(&i18n::t("action-back"), "pill-quiet"); onboard_created.append(&saved_button); onboard_created.append(&created_back); stack.add_named(¢er(&onboard_created), Some("onboard-created")); let onboard_restore = gtk::Box::new(Orientation::Vertical, 12); - onboard_restore.append(&label("Restore from a seed", "subtitle")); + onboard_restore.append(&label(&i18n::t("onboard-restore-title"), "subtitle")); let restore_list = boxed_list(); let onboard_seed_entry = adw::EntryRow::builder() - .title("Seed — 32 bytes, base32 or hex") + .title(i18n::t("onboard-seed-entry-title")) .build(); restore_list.append(&onboard_seed_entry); onboard_restore.append(&restore_list); - let restore_go = button("Restore", "pill"); - let restore_back = button("Back", "pill-quiet"); + let restore_go = button(&i18n::t("onboard-restore-go-button"), "pill"); + let restore_back = button(&i18n::t("action-back"), "pill-quiet"); onboard_restore.append(&restore_go); onboard_restore.append(&restore_back); stack.add_named(¢er(&onboard_restore), Some("onboard-restore")); // signup: step 1 — the server this address lives on let signup_server = gtk::Box::new(Orientation::Vertical, 12); - signup_server.append(&label("Set up the server", "subtitle")); + signup_server.append(&label(&i18n::t("signup-server-title"), "subtitle")); let signup_server_group = adw::PreferencesGroup::new(); - let signup_host = adw::EntryRow::builder().title("Host").build(); + let signup_host = adw::EntryRow::builder() + .title(i18n::t("settings-field-host")) + .build(); signup_server_group.add(&signup_host); signup_server.append(&signup_server_group); @@ -997,31 +1007,27 @@ impl CarrierWidgets { signup_key_card.append(&signup_key_value); signup_key_card.append(&signup_key_fingerprint); signup_server.append(&signup_key_card); - let signup_key_hint = label( - "If the server operator published a key or fingerprint, make \ - sure it matches before trusting it.", - "dim", - ); + let signup_key_hint = label(&i18n::t("trust-fingerprint-hint"), "dim"); signup_key_hint.set_wrap(true); signup_key_hint.set_visible(false); signup_server.append(&signup_key_hint); - let signup_key_button = button("Fetch the server's key", "pill"); - let signup_start_over = button("Start over", "pill-quiet"); + let signup_key_button = button(&i18n::t("signup-fetch-key-button"), "pill"); + let signup_start_over = button(&i18n::t("signup-start-over-button"), "pill-quiet"); signup_server.append(&signup_key_button); signup_server.append(&signup_start_over); stack.add_named(¢er(&signup_server), Some("signup-server")); // signup: step 2 — bind a username, or recall an existing address let signup_account = gtk::Box::new(Orientation::Vertical, 12); - signup_account.append(&label("Choose your address", "subtitle")); + signup_account.append(&label(&i18n::t("signup-account-title"), "subtitle")); let signup_mode_new = gtk::ToggleButton::builder() - .label("Create a new address") + .label(i18n::t("signup-mode-new-label")) .active(true) .hexpand(true) .build(); let signup_mode_existing = gtk::ToggleButton::builder() - .label("I already have this address") + .label(i18n::t("signup-mode-existing-label")) .hexpand(true) .build(); signup_mode_existing.set_group(Some(&signup_mode_new)); @@ -1032,18 +1038,20 @@ impl CarrierWidgets { signup_mode_box.append(&signup_mode_existing); signup_account.append(&signup_mode_box); let signup_account_group = adw::PreferencesGroup::new(); - let signup_user = adw::EntryRow::builder().title("User").build(); + let signup_user = adw::EntryRow::builder() + .title(i18n::t("signup-user-label")) + .build(); let signup_invite = adw::EntryRow::builder() - .title("Invite token (if required by the server)") + .title(i18n::t("signup-invite-label")) .build(); signup_account_group.add(&signup_user); signup_account_group.add(&signup_invite); signup_account.append(&signup_account_group); - let register_button = button("Register", "pill"); - let recall_button = button("Restore access", "pill"); + let register_button = button(&i18n::t("signup-register-button"), "pill"); + let recall_button = button(&i18n::t("signup-recall-button"), "pill"); signup_account.append(®ister_button); signup_account.append(&recall_button); - let signup_step_back = button("Back", "quiet"); + let signup_step_back = button(&i18n::t("action-back"), "quiet"); signup_account.append(&signup_step_back); stack.add_named(¢er(&signup_account), Some("signup-account")); @@ -1057,7 +1065,7 @@ impl CarrierWidgets { spinner.set_visible(false); let status_label = label("", "caption"); status_label.set_hexpand(true); - let cancel_button = button("Cancel", ""); + let cancel_button = button(&i18n::t("action-cancel"), ""); cancel_button.set_visible(false); status_bar.append(&spinner); status_bar.append(&status_label); @@ -1221,7 +1229,7 @@ impl CarrierWidgets { let export_window = root.clone(); export_row.connect_activated(move |_| { let dialog = gtk::FileDialog::builder() - .title("Export Backup") + .title(i18n::t("backup-export-dialog-title")) .initial_name("carrier-backup.gsmol") .build(); let sender = export_sender.clone(); @@ -1240,7 +1248,9 @@ impl CarrierWidgets { let backup_import_sender = sender.clone(); let backup_import_window = root.clone(); import_row.connect_activated(move |_| { - let dialog = gtk::FileDialog::builder().title("Import Backup").build(); + let dialog = gtk::FileDialog::builder() + .title(i18n::t("backup-import-dialog-title")) + .build(); let sender = backup_import_sender.clone(); dialog.open( Some(&backup_import_window), @@ -1445,11 +1455,13 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { }), PendingOp::Register { addr, invite } => sender.spawn_command(move |out| { let res = ops::register(&store, &master, &addr, invite.as_deref(), timeout); - let _ = out.send(OpResult::Generic(res.map(|_| "registered".to_string()))); + let _ = out.send(OpResult::Generic( + res.map(|_| i18n::t("status-registered-note")), + )); }), PendingOp::Restore { addr } => sender.spawn_command(move |out| { let res = ops::restore(&store, &master, &addr, timeout) - .map(|index| format!("restored at rotation {index}")); + .map(|index| i18n::t1("status-restored-note", "index", index as i64)); let _ = out.send(OpResult::Generic(res)); }), PendingOp::LearnKey { addr } => sender.spawn_command(move |out| { @@ -1507,32 +1519,36 @@ fn spawn_op(model: &Carrier, op: PendingOp, sender: &ComponentSender) { everywhere, } => sender.spawn_command(move |out| { let res = ops::delete(&store, &master, &id, folder, everywhere, timeout) - .map(|_| "deleted".to_string()); + .map(|_| i18n::t("status-deleted-note")); let _ = out.send(OpResult::Generic(res)); }), PendingOp::Accept { address, identity } => sender.spawn_command(move |out| { let res = ops::accept_contact(&store, &master, &address, &identity, timeout) - .map(|_| format!("accepted {address}")); + .map(|_| i18n::t1("status-accepted-note", "address", address.clone())); let _ = out.send(OpResult::Generic(res)); }), PendingOp::Block { address } => sender.spawn_command(move |out| { let res = ops::block_contact(&store, &master, &address, timeout).map(|blocked| { if blocked { - format!("blocked {address}") + i18n::t1("status-blocked-note", "address", address.clone()) } else { - format!("{address} was not blocked") + i18n::t1("status-not-blocked-note", "address", address.clone()) } }); let _ = out.send(OpResult::Generic(res)); }), PendingOp::ImportContact { uri } => sender.spawn_command(move |out| { let res = ops::import_contact(&store, &uri) - .map(|(address, _)| format!("imported {address} (verified)")); + .map(|(address, _)| i18n::t1("status-imported-note", "address", address)); let _ = out.send(OpResult::Generic(res)); }), PendingOp::Rotate => sender.spawn_command(move |out| { - let res = ops::rotate(&store, &master, timeout) - .map(|(index, key)| format!("rotated to index {index}; new key {}…", &key[..12])); + let res = ops::rotate(&store, &master, timeout).map(|(index, key)| { + let mut args = fluent_bundle::FluentArgs::new(); + args.set("index", index as i64); + args.set("key_prefix", key[..12].to_string()); + i18n::t_args("status-rotated-note", Some(&args)) + }); let _ = out.send(OpResult::Generic(res)); }), PendingOp::Export { path } => sender.spawn_command(move |out| { @@ -1607,23 +1623,26 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { w.compose_pin_key.set_text(&b32(&key)); w.compose_pin_fingerprint.set_text(&fingerprint(&key)); - w.compose_pin_button.set_label("Pin and send"); + w.compose_pin_button + .set_label(&i18n::t("compose-pin-button-send")); } - None => w.compose_pin_button.set_label("Fetch their server's key"), + None => w + .compose_pin_button + .set_label(&i18n::t("compose-pin-button-fetch")), } let fetched = prompt.offered.is_some(); for line in [ @@ -1702,27 +1721,31 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender (None, "Inbox is empty", "Fetch to check the mailbox."), + let (icon, title_id, description_id) = match model.folder { + Folder::Inbox => ( + None, + "mail-inbox-empty-title", + "mail-inbox-empty-description", + ), Folder::Requests => ( Some("contact-new-symbolic"), - "No requests", - "Mail from senders you have not accepted yet arrives here.", + "mail-requests-empty-title", + "mail-requests-empty-description", ), Folder::Sent => ( Some("mail-send-symbolic"), - "Nothing sent yet", - "Messages you send keep a sealed copy here.", + "mail-sent-empty-title", + "mail-sent-empty-description", ), Folder::Drafts => ( Some("document-edit-symbolic"), - "No drafts", - "A compose closed from the sidebar is kept here.", + "mail-drafts-empty-title", + "mail-drafts-empty-description", ), }; w.list_empty.set_icon_name(icon); - w.list_empty.set_title(title); - w.list_empty.set_description(Some(description)); + w.list_empty.set_title(&i18n::t(title_id)); + w.list_empty.set_description(Some(&i18n::t(description_id))); let inbox_empty = model.folder == Folder::Inbox; w.list_empty_fetch.set_visible(inbox_empty); w.list_empty_hint @@ -1768,8 +1791,11 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { - let block = icon_label_button("action-unavailable-symbolic", "Block", "danger"); + let block = icon_label_button( + "action-unavailable-symbolic", + &i18n::t("action-block"), + "danger", + ); let msg = Message::ContactBlock(contact.address.clone()); let block_sender = sender.clone(); block.connect_clicked(move |_| block_sender.input(msg.clone())); w.actions.append(&block); } Some(false) => { - let unblock = icon_label_button("object-select-symbolic", "Unblock", "accent"); + let unblock = icon_label_button( + "object-select-symbolic", + &i18n::t("action-unblock"), + "accent", + ); let msg = Message::ContactUnblock(contact.address.clone()); let unblock_sender = sender.clone(); unblock.connect_clicked(move |_| unblock_sender.input(msg.clone())); w.actions.append(&unblock); } None => { - let accept = icon_label_button("object-select-symbolic", "Accept", "accent"); + let accept = icon_label_button( + "object-select-symbolic", + &i18n::t("action-accept"), + "accent", + ); let msg = Message::ContactAccept(contact.address.clone(), contact.key.clone()); let accept_sender = sender.clone(); accept.connect_clicked(move |_| accept_sender.input(msg.clone())); @@ -1876,16 +1922,16 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender{address}") + let from = if known { + format!("{address}") } else { - format!("{label} {address}") + address }; - meta.push_str(&format!(" · {} UTC", escape(&ops::when(msg.time)))); - meta.push_str(&format!(" · id {}…", &msg.id[..msg.id.len().min(8)])); - if msg.kept { - meta.push_str(" · a server copy still exists"); - } + let mut meta_args = fluent_bundle::FluentArgs::new(); + meta_args.set("label", label); + meta_args.set("from", from); + meta_args.set("time", escape(&ops::when(msg.time))); + meta_args.set("id", msg.id[..msg.id.len().min(8)].to_string()); + meta_args.set("kept", if msg.kept { "yes" } else { "no" }); + let meta = i18n::t_args("mail-meta-line", Some(&meta_args)); w.article_meta.set_markup(&meta); let headers: Vec = msg .headers @@ -1914,20 +1962,29 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let alert = adw::AlertDialog::new( - Some("Delete this message?"), - Some(if *kept { - "Deleting it here leaves the server's copy in place. Deleting it everywhere asks the server to drop that copy too." + Some(&i18n::t("delete-dialog-title")), + Some(&i18n::t(if *kept { + "delete-dialog-body-kept" } else { - "The sealed copy in the local store is removed." - }), + "delete-dialog-body-gone" + })), ); - alert.add_response("cancel", "Cancel"); - alert.add_response("here", "Delete"); + alert.add_response("cancel", &i18n::t("action-cancel")); + alert.add_response("here", &i18n::t("action-delete")); alert.set_response_appearance("here", adw::ResponseAppearance::Destructive); if *kept { - alert.add_response("everywhere", "Delete Everywhere"); + alert.add_response( + "everywhere", + &i18n::t("delete-dialog-everywhere-button"), + ); alert.set_response_appearance( "everywhere", adw::ResponseAppearance::Destructive, @@ -2124,21 +2191,21 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let alert = adw::AlertDialog::new( - Some("Rotate the identity?"), - Some("Issues a fresh key from your secret with a signed certificate proving it replaces this one. Contacts accept the change from that chain, and the old key stays readable. Rotation is not revocation."), + Some(&i18n::t("rotate-dialog-title")), + Some(&i18n::t("advanced-rotations-description")), ); - alert.add_response("cancel", "Cancel"); - alert.add_response("rotate", "Rotate"); + alert.add_response("cancel", &i18n::t("action-cancel")); + alert.add_response("rotate", &i18n::t("advanced-rotate-button")); alert.set_response_appearance("rotate", adw::ResponseAppearance::Destructive); alert } Dialog::Reset => { let alert = adw::AlertDialog::new( - Some("Log out and delete all data?"), - Some("Removes this identity from this client entirely: the secret, pins, contacts and every cached message. Without a backup of the secret there is no way back in. Mail already fetched here may have no other copy anywhere, unless \"leave mail on server\" was on. Whatever is still on the server comes back on the first fetch after logging back in."), + Some(&i18n::t("reset-dialog-title")), + Some(&i18n::t("advanced-logout-description")), ); - alert.add_response("cancel", "Cancel"); - alert.add_response("reset", "Log Out"); + alert.add_response("cancel", &i18n::t("action-cancel")); + alert.add_response("reset", &i18n::t("advanced-logout-button")); alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive); alert } @@ -2149,14 +2216,12 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender>() .join("\n"); let alert = adw::AlertDialog::new( - Some("Carry the sender's fields into the reply?"), - Some(&format!( - "This message carries its own frontmatter:\n\n{list}\n\nReply with copies of these fields, or start with none. The fields stay editable either way." - )), + Some(&i18n::t("reply-dialog-title")), + Some(&i18n::t1("reply-dialog-body", "list", list)), ); - alert.add_response("cancel", "Cancel Reply"); - alert.add_response("without", "Reply Without"); - alert.add_response("copy", "Copy Fields"); + alert.add_response("cancel", &i18n::t("reply-dialog-cancel-button")); + alert.add_response("without", &i18n::t("reply-dialog-without-button")); + alert.add_response("copy", &i18n::t("reply-dialog-copy-button")); alert.set_response_appearance("copy", adw::ResponseAppearance::Suggested); alert } @@ -2166,14 +2231,16 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { + let mut args = fluent_bundle::FluentArgs::new(); + args.set("address", address.clone()); + args.set("known_fingerprint", known_fingerprint.clone()); + args.set("offered_fingerprint", offered_fingerprint.clone()); let alert = adw::AlertDialog::new( - Some("Replace this contact's key?"), - Some(&format!( - "{address} is already on file with a different key.\n\nknown {known_fingerprint}\nnew {offered_fingerprint}\n\nThe address you were given carries the new key and would replace the stored one. If this contact did not hand it to you themselves, stop: a key swapped in on the way reads everything you send them." - )), + Some(&i18n::t("key-change-dialog-title")), + Some(&i18n::t_args("key-change-dialog-body", Some(&args))), ); - alert.add_response("cancel", "Cancel"); - alert.add_response("replace", "Replace Key"); + alert.add_response("cancel", &i18n::t("action-cancel")); + alert.add_response("replace", &i18n::t("key-change-dialog-replace-button")); alert.set_response_appearance("replace", adw::ResponseAppearance::Destructive); alert } @@ -2231,9 +2298,15 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender { let inbox = model.lists.first().map(|rows| rows.len()).unwrap_or(0); - format!("fetching… {inbox} in inbox") + i18n::t1("status-fetching-progress", "count", inbox as i64) } - (Some(busy), false) => format!("{busy}…"), + (Some(busy), false) => match busy.as_str() { + "fetching" => i18n::t("status-busy-fetching"), + "refreshing" => i18n::t("status-busy-refreshing"), + "registering" => i18n::t("status-busy-registering"), + "restoring" => i18n::t("status-busy-restoring"), + other => format!("{other}…"), + }, (None, _) => model.status.clone().unwrap_or_default(), }); let errored = model.status_error && model.busy.is_none(); @@ -2256,7 +2329,7 @@ fn mail_row(folder: Folder, row: &Row) -> adw::ActionRow { "mail-read-symbolic" })); let subject = if row.subject.is_empty() { - "(no subject)".to_string() + i18n::t("mail-no-subject") } else { row.subject.clone() }; @@ -2295,7 +2368,7 @@ fn compose_header_row<'a>( ) -> (gtk::ListBoxRow, gtk::Entry) { let entry = gtk::Entry::new(); entry.set_hexpand(true); - entry.set_placeholder_text(Some("Key: value")); + entry.set_placeholder_text(Some(&i18n::t("compose-header-placeholder"))); entry.set_text(text); let style = |entry: >k::Entry, text: &str| match crate::ops::header_row(text) { Err(reason) => { @@ -2316,7 +2389,10 @@ fn compose_header_row<'a>( change_sender.input(Message::ComposeHeaderChanged(index, text)); }); - let remove = icon_button("user-trash-symbolic", "Remove this header"); + let remove = icon_button( + "user-trash-symbolic", + &i18n::t("compose-header-remove-tooltip"), + ); let remove_sender = sender.clone(); remove.connect_clicked(move |_| { remove_sender.input(Message::ComposeHeaderRemoved(index)); @@ -2352,7 +2428,11 @@ fn sidebar_contact_row(contact: &Contact) -> adw::ActionRow { /// The detail view's body: the three lines an address is made of, in /// the column-aligned form a key block wants. fn detail_body(address: &str, key: &str, fingerprint: &str) -> String { - format!("key {key}\nfingerprint {fingerprint}\nsmol://{address}/{key}") + format!( + "{:<14}{key}\n{:<14}{fingerprint}\nsmol://{address}/{key}", + i18n::t("detail-key-label"), + i18n::t("detail-fingerprint-label"), + ) } /// Puts one URI's QR code into the detail header's share popover. A From 06952cd20e201d0b61fdca8fb2317812a8727d4c Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 22:27:25 +0300 Subject: [PATCH 27/37] docs: rewrite catalogue copy to the Microsoft Writing Style Guide --- locales/en-US/main.ftl | 134 ++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 03a3bc1..764ee53 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -3,33 +3,33 @@ ## Seed parsing — restoring an identity from a pasted seed -seed-bad-hex = bad hex -seed-wrong-length = a seed is 32 bytes +seed-bad-hex = That's not valid hex. Check for typos or extra characters. +seed-wrong-length = A seed is 32 bytes. Check you copied the whole thing. ## Local files — errors reading/writing the key and its sidecars file-error = { $path }: { $reason } -key-file-wrong-length = { $path }: not a 32-byte master +key-file-wrong-length = { $path }: not a 32-byte secret ## Compose — validating a typed `Key: value` header row -compose-header-colon-missing = a header is `Key: value` — the colon is missing -compose-header-reserved-key = { $key } is attached by the client and cannot be set -compose-header-invalid-key = keys are 1-64 characters of letters, digits and dashes -compose-header-value-multiline = a value must stay on one line -compose-header-duplicate-key = { $key } is set twice -compose-headers-too-large = the headers exceed the frontmatter budget -compose-headers-too-many = too many headers +compose-header-colon-missing = Headers need a colon, like `Key: value`. +compose-header-reserved-key = { $key } is set automatically and can't be changed. +compose-header-invalid-key = Keys can be 1 to 64 letters, digits, and dashes. +compose-header-value-multiline = Values need to stay on one line. +compose-header-duplicate-key = { $key } is set twice. Remove one of them. +compose-headers-too-large = Your headers take up too much space. Remove some and try again. +compose-headers-too-many = You have too many headers. Remove some and try again. ## Contacts — importing a smol:// address -contacts-import-needs-key = import needs a self-certifying smol:// address carrying a key +contacts-import-needs-key = Import needs a smol:// address that includes a key. ## Mail — opening a message by id -mail-message-id-wrong-length = message ids are 64 hex characters -mail-message-id-wrong-bytes = message ids are 32 bytes -mail-message-not-found = message not found +mail-message-id-wrong-length = Message IDs are 64 hex characters. +mail-message-id-wrong-bytes = Message IDs are 32 bytes. +mail-message-not-found = Message not found. ## Fetch — the status note after a fetch completes ## @@ -67,51 +67,51 @@ send-result-note = { $variant -> ## Built in `spawn_op` (src/view.rs) rather than ops.rs, since that is ## where each `PendingOp` result is assembled for the worker thread. -status-registered-note = registered -status-restored-note = restored at rotation { $index } -status-deleted-note = deleted -status-accepted-note = accepted { $address } -status-blocked-note = blocked { $address } +status-registered-note = Registered +status-restored-note = Restored at rotation { $index } +status-deleted-note = Deleted +status-accepted-note = Accepted { $address } +status-blocked-note = Blocked { $address } status-not-blocked-note = { $address } was not blocked -status-imported-note = imported { $address } (verified) -status-rotated-note = rotated to index { $index }; new key { $key_prefix }… +status-imported-note = Imported { $address } (verified) +status-rotated-note = Rotated to index { $index }; new key { $key_prefix }… ## Backup -backup-written-note = backup written to { $path } +backup-written-note = Backup written to { $path } ## Drafts -draft-saved-note = draft saved +draft-saved-note = Draft saved ## Log out -logged-out-note = logged out. Create a new identity or restore a seed. +logged-out-note = Signed out. Create a new identity or restore a seed. ## Onboarding -identity-created-note = Identity created. Write the seed down; it is the only secret. +identity-created-note = Identity created. Write the seed down. It's the only secret. seed-restored-note = Seed restored. Bind a username or recall an address. ## Signup -signup-enter-host-error = enter the server host first -signup-enter-user-host-error = enter user and host +signup-enter-host-error = Enter the server host first. +signup-enter-user-host-error = Enter the user and host. ## Compose -compose-enter-recipient-error = enter a recipient -compose-header-row-error = header { $index }: { $reason } -reply-no-address-error = this sender left no address to reply to +compose-enter-recipient-error = Enter a recipient. +compose-header-row-error = Header { $index }: { $reason } +reply-no-address-error = This sender left no address to reply to. reply-subject-prefix = Re: { $subject } -address-invalid-error = that address does not parse +address-invalid-error = That address isn't in the right format. ## Contacts -contacts-reply-address-invalid-error = the sender's reply address does not parse -contacts-sender-key-invalid-error = the sender's key does not parse -contacts-accept-no-address-error = this sender left no address to accept -contacts-block-no-address-error = this sender left no address to block +contacts-reply-address-invalid-error = The sender's reply address isn't in the right format. +contacts-sender-key-invalid-error = The sender's key isn't in the right format. +contacts-accept-no-address-error = This sender left no address to accept. +contacts-block-no-address-error = This sender left no address to block. ## Common — short words reused verbatim across several buttons/dialogs @@ -153,7 +153,7 @@ mail-meta-line = { $kept -> [yes] { $label } { $from } · { $time } UTC · id { $id }… · a server copy still exists } mail-no-subject = (no subject) -mail-no-reply-address = no reply address: cannot accept or block +mail-no-reply-address = no reply address: can't accept or block contacts-empty-hint = no contacts ## Mail list — empty-folder messages, and the own-identity detail @@ -161,11 +161,11 @@ contacts-empty-hint = no contacts mail-inbox-empty-title = Inbox is empty mail-inbox-empty-description = Fetch to check the mailbox. mail-requests-empty-title = No requests -mail-requests-empty-description = Mail from senders you have not accepted yet arrives here. +mail-requests-empty-description = Mail from senders you haven't accepted yet arrives here. mail-sent-empty-title = Nothing sent yet mail-sent-empty-description = Messages you send keep a sealed copy here. mail-drafts-empty-title = No drafts -mail-drafts-empty-description = A compose closed from the sidebar is kept here. +mail-drafts-empty-description = Closing a compose from the sidebar keeps it here. own-identity-meta = you · rotation { $rotations } contacts-pin-button-fetched = Pin this key contacts-pin-button-unfetched = Pin server @@ -224,7 +224,7 @@ settings-field-key = Key settings-field-fingerprint = Fingerprint settings-field-uri = URI property-row-copy-tooltip = Copy the { $field } -settings-recovery-secret-title = Recovery Secret +settings-recovery-secret-title = Recovery secret settings-secret-reveal-tooltip = Reveal the recovery secret settings-secret-hide-tooltip = Hide the recovery secret settings-secret-copy-tooltip = Copy the recovery secret @@ -232,24 +232,24 @@ settings-secret-copy-tooltip = Copy the recovery secret ## Settings — Server tab settings-field-host = Host -settings-field-public-key = Public Key +settings-field-public-key = Public key settings-public-key-not-pinned = not pinned yet server-connection-title = Connection server-timeout-title = Timeout server-timeout-subtitle = Seconds before a network call gives up server-fetching-title = Fetching server-leave-mail-title = Leave mail on server -server-leave-mail-subtitle = Keep a copy on the server after fetching, so another client can fetch it too. When this is enabled, deleting a message removes it from this client and the server. +server-leave-mail-subtitle = Keep a copy on the server after fetching, so you can fetch it again from another app or device. When this is on, deleting a message removes it here and on the server. ## Settings — Backup tab -backup-page-description = One file holding your mail, contacts and server info for moving to another client, or for keeping what the server no longer has. It is sealed to this identity and deliberately leaves the secret itself out. +backup-page-description = One file with your mail, contacts, and server info, for moving to another app or keeping what the server no longer has. It's sealed to this identity and never includes the secret itself. backup-export-title = Export backup-export-subtitle = Write a backup file backup-import-title = Import backup-import-subtitle = Restore from a backup file -backup-export-dialog-title = Export Backup -backup-import-dialog-title = Import Backup +backup-export-dialog-title = Export backup +backup-import-dialog-title = Import backup ## Settings — Advanced tab @@ -257,13 +257,13 @@ advanced-rotations-title = Rotations advanced-rotations-description = Issues a fresh key from your secret with a signed certificate proving it replaces this one. Contacts accept the change from that chain, and the old key stays readable. Rotation is not revocation. advanced-rotations-completed = Completed advanced-rotate-button = Rotate identity -advanced-first-contact-title = First Contact -advanced-first-contact-description = A recipient who is not a contact yet, and whose address carries no key, has to be looked up on their own server. That answer is only as good as the server: one with no pinned key can return a key of its own and read what you send. +advanced-first-contact-title = First contact +advanced-first-contact-description = A recipient who isn't a contact yet, and whose address carries no key, has to be looked up on their own server. That answer is only as good as the server: one with no pinned key can return a key of its own and read what you send. advanced-strict-pinning-title = Require a pinned server -advanced-strict-pinning-subtitle = Stop such a send and offer to pin the server's key first, so it can be checked against what the operator published. With this off, the key is taken on trust and the send is only reported as unverified afterwards. -advanced-logout-title = Log Out -advanced-logout-description = Removes this identity from this client entirely: the secret, pins, contacts and every cached message. Without a backup of the secret there is no way back in. Mail already fetched here may have no other copy anywhere, unless "leave mail on server" was on. Whatever is still on the server comes back on the first fetch after logging back in. -advanced-logout-button = Log out and delete all data +advanced-strict-pinning-subtitle = Stop that send and offer to pin the server's key first, so you can check it against what the operator published. With this off, the key's taken on trust, and the send is only reported as unverified afterwards. +advanced-logout-title = Sign out +advanced-logout-description = Removes this identity from this app entirely: the secret, pins, contacts, and every cached message. Without a backup of the secret, you can't get back in. Mail already fetched here may have no other copy anywhere, unless "Leave mail on server" was on. Whatever's still on the server comes back on the first fetch after you sign back in. +advanced-logout-button = Sign out and delete all data ## Onboarding ## @@ -272,12 +272,12 @@ advanced-logout-button = Log out and delete all data onboard-tagline = smolmail: one seed, sealed mail, five operations onboard-create-button = Create a new identity -onboard-restore-button = Restore existing identity from seed +onboard-restore-button = Restore an existing identity from a seed onboard-created-title = Write this seed down -onboard-created-description = It is the only secret, and it is shown once. +onboard-created-description = It's the only secret, and it's shown once. onboard-saved-button = I saved it onboard-restore-title = Restore from a seed -onboard-seed-entry-title = Seed — 32 bytes, base32 or hex +onboard-seed-entry-title = Seed: 32 bytes, base32 or hex onboard-restore-go-button = Restore ## Signup @@ -290,14 +290,14 @@ signup-account-title = Choose your address signup-mode-new-label = Create a new address signup-mode-existing-label = I already have this address signup-user-label = User -signup-invite-label = Invite token (if required by the server) +signup-invite-label = Invite token (if the server requires one) signup-register-button = Register signup-recall-button = Restore access ## Compose — the inline pin card shown after a blocked send -compose-pin-title = { $host } is not pinned -compose-pin-body = { $address } is not a contact yet, so carrier would have to ask this server for their key — and a server with no pinned key can answer with one of its own and read the message. +compose-pin-title = { $host } isn't pinned +compose-pin-body = { $address } isn't a contact yet, so carrier would have to ask this server for its key. A server with no pinned key can answer with one of its own, then read the message. compose-pin-button-fetch = Fetch their server's key compose-pin-button-send = Pin and send @@ -305,21 +305,21 @@ compose-pin-button-send = Pin and send delete-dialog-title = Delete this message? delete-dialog-body-kept = Deleting it here leaves the server's copy in place. Deleting it everywhere asks the server to drop that copy too. -delete-dialog-body-gone = The sealed copy in the local store is removed. -delete-dialog-everywhere-button = Delete Everywhere +delete-dialog-body-gone = This removes the only copy, which is sealed and stored on this device. +delete-dialog-everywhere-button = Delete everywhere rotate-dialog-title = Rotate the identity? -reset-dialog-title = Log out and delete all data? +reset-dialog-title = Sign out and delete all data? # `$list` is the sender's own frontmatter rows, already typed by a human # and reproduced verbatim — not translatable prose itself, just data # interpolated into this one sentence. reply-dialog-title = Carry the sender's fields into the reply? -reply-dialog-body = This message carries its own frontmatter:{"\u000A"}{"\u000A"}{ $list }{"\u000A"}{"\u000A"}Reply with copies of these fields, or start with none. The fields stay editable either way. -reply-dialog-cancel-button = Cancel Reply -reply-dialog-without-button = Reply Without -reply-dialog-copy-button = Copy Fields +reply-dialog-body = This message has its own headers:{"\u000A"}{"\u000A"}{ $list }{"\u000A"}{"\u000A"}Reply with copies of these fields, or start with none. The fields stay editable either way. +reply-dialog-cancel-button = Cancel reply +reply-dialog-without-button = Reply without +reply-dialog-copy-button = Copy fields key-change-dialog-title = Replace this contact's key? -key-change-dialog-body = { $address } is already on file with a different key.{"\u000A"}{"\u000A"}known { $known_fingerprint }{"\u000A"}new { $offered_fingerprint }{"\u000A"}{"\u000A"}The address you were given carries the new key and would replace the stored one. If this contact did not hand it to you themselves, stop: a key swapped in on the way reads everything you send them. -key-change-dialog-replace-button = Replace Key +key-change-dialog-body = { $address } is already on file with a different key.{"\u000A"}{"\u000A"}known { $known_fingerprint }{"\u000A"}new { $offered_fingerprint }{"\u000A"}{"\u000A"}The address you were given carries the new key and would replace the one on file. If this contact didn't hand it to you themselves, stop: a key swapped in on the way reads everything you send them. +key-change-dialog-replace-button = Replace key ## Status bar ## @@ -337,7 +337,7 @@ status-fetching-progress = fetching… { $count } in inbox ## First contact / server trust -first-contact-blocked-error = { $address } is not a contact yet and { $host } is not pinned — pin their server's key to send. +first-contact-blocked-error = { $address } isn't a contact yet, and { $host } isn't pinned. Pin their server's key to send. server-key-offered-note = The server presented this key. Pin it only if the fingerprint matches what the operator published. server-key-already-pinned-note = This server's key is already pinned. -server-key-pinned-note = server key pinned +server-key-pinned-note = Server key pinned From c2525fd0f04bc88a02b6e5d6ddf3bb955e2aa3cb Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 22:32:52 +0300 Subject: [PATCH 28/37] refactor: flatten Fluent selectors into one message per variant --- locales/en-US/main.ftl | 66 +++++++++++++++++++----------------------- src/ops.rs | 12 ++++---- src/view.rs | 18 ++++++++---- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 764ee53..da95583 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -33,34 +33,32 @@ mail-message-not-found = Message not found. ## Fetch — the status note after a fetch completes ## -## `$variant` picks the one combination of "N rejected" and "(cancelled)" -## that applies, so each case reads as one complete sentence rather than -## stitched-together fragments. +## Split from one selector into one message per combination of "N +## rejected" and "(cancelled)" that can apply; the caller in ops.rs +## picks the id to match, since a flat message can't branch on a +## variable itself. -fetch-result-note = { $variant -> - [rejected] { $total } fetched, { $stored } stored, { $rejected } rejected - [cancelled] { $total } fetched, { $stored } stored (cancelled) - [rejected-cancelled] { $total } fetched, { $stored } stored, { $rejected } rejected (cancelled) - *[plain] { $total } fetched, { $stored } stored -} +fetch-result-note-rejected = { $total } fetched, { $stored } stored, { $rejected } rejected +fetch-result-note-cancelled = { $total } fetched, { $stored } stored (cancelled) +fetch-result-note-rejected-cancelled = { $total } fetched, { $stored } stored, { $rejected } rejected (cancelled) +fetch-result-note-plain = { $total } fetched, { $stored } stored ## Send — the status note after a send completes ## -## `$variant` picks the one combination of trust-change clause and -## accept-token clause that applies, each spelled out as a full sentence. -## Any warning fumi itself reports is appended separately in code, since -## that text is fumi's own and not part of this catalogue. +## Split from one selector into one message per combination of +## trust-change clause and accept-token clause that can apply; the +## caller in ops.rs picks the id to match. Any warning fumi itself +## reports is appended separately in code, since that text is fumi's +## own and not part of this catalogue. -send-result-note = { $variant -> - [token] Sent to { $address } ({ $bytes } bytes), accept token attached - [unverified] Sent to { $address } ({ $bytes } bytes); new contact learned from an unpinned server (unverified) - [unverified-token] Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned from an unpinned server (unverified) - [tofu] Sent to { $address } ({ $bytes } bytes); new contact learned (trust on first use) - [tofu-token] Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned (trust on first use) - [rotated] Sent to { $address } ({ $bytes } bytes); contact rotated to a new key (chain verified) - [rotated-token] Sent to { $address } ({ $bytes } bytes), accept token attached; contact rotated to a new key (chain verified) - *[plain] Sent to { $address } ({ $bytes } bytes) -} +send-result-note-token = Sent to { $address } ({ $bytes } bytes), accept token attached +send-result-note-unverified = Sent to { $address } ({ $bytes } bytes); new contact learned from an unpinned server (unverified) +send-result-note-unverified-token = Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned from an unpinned server (unverified) +send-result-note-tofu = Sent to { $address } ({ $bytes } bytes); new contact learned (trust on first use) +send-result-note-tofu-token = Sent to { $address } ({ $bytes } bytes), accept token attached; new contact learned (trust on first use) +send-result-note-rotated = Sent to { $address } ({ $bytes } bytes); contact rotated to a new key (chain verified) +send-result-note-rotated-token = Sent to { $address } ({ $bytes } bytes), accept token attached; contact rotated to a new key (chain verified) +send-result-note-plain = Sent to { $address } ({ $bytes } bytes) ## Background-operation status notes ## @@ -143,15 +141,13 @@ contacts-import-button = Import ## Mail — the open-message meta line ## -## `$kept` picks the one variant that applies, each a full sentence, -## rather than an optional clause spliced onto a shared prefix. +## Split from one selector into one message per variant, each a full +## sentence; the caller in view.rs picks `-yes`/`-no` to match `$kept`. mail-meta-to = to mail-meta-from = from -mail-meta-line = { $kept -> - *[no] { $label } { $from } · { $time } UTC · id { $id }… - [yes] { $label } { $from } · { $time } UTC · id { $id }… · a server copy still exists -} +mail-meta-line-no = { $label } { $from } · { $time } UTC · id { $id }… +mail-meta-line-yes = { $label } { $from } · { $time } UTC · id { $id }… · a server copy still exists mail-no-subject = (no subject) mail-no-reply-address = no reply address: can't accept or block contacts-empty-hint = no contacts @@ -173,8 +169,7 @@ contacts-pin-button-unfetched = Pin server mail-empty-title = No message selected mail-empty-description = Pick a message from the list above to read it. # The two sentences shown under Fetch when the inbox is empty. -mail-empty-hint = Fetching removes mail from the server once it's stored here. - Turn on "Leave mail on server" in Settings to keep it there instead. +mail-empty-hint = Fetching removes mail from the server once it's stored here. Turn on "Leave mail on server" in Settings to keep it there instead. detail-qr-tooltip = Show the address as a QR code detail-copy-tooltip = Copy the address @@ -194,11 +189,10 @@ contact-state-blocked = blocked contact-state-verified = verified contact-state-tofu = trust on first use # `$state` is one of the four words above, already resolved by the caller -# through `contact_state`; this message only adds the server's pin state. -contact-meta-line = { $state } · server { $pinned -> - [yes] pinned - *[no] not pinned -} +# through `contact_state`. Split from one selector into one message per +# pin state; the caller in view.rs picks `-yes`/`-no` to match `$pinned`. +contact-meta-line-yes = { $state } · server pinned +contact-meta-line-no = { $state } · server not pinned ## Detail pane — the monospace key/value block shared by contacts and the ## own identity view. Column widths are padded to these labels in code diff --git a/src/ops.rs b/src/ops.rs index 5226335..6be3797 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -509,11 +509,13 @@ pub fn fetch( (false, false) => "plain", }; let mut args = fluent_bundle::FluentArgs::new(); - args.set("variant", variant); args.set("total", fetched.total as i64); args.set("stored", fetched.stored as i64); args.set("rejected", fetched.rejected.len() as i64); - Ok(i18n::t_args("fetch-result-note", Some(&args))) + Ok(i18n::t_args( + &format!("fetch-result-note-{variant}"), + Some(&args), + )) } /// Fields fumi itself attaches to every message (sec 5.7, 5.8); a @@ -634,10 +636,9 @@ pub fn send( (None, false) => "plain".to_string(), }; let mut args = fluent_bundle::FluentArgs::new(); - args.set("variant", variant); args.set("address", addr.short()); args.set("bytes", sent.bytes as i64); - let mut note = i18n::t_args("send-result-note", Some(&args)); + let mut note = i18n::t_args(&format!("send-result-note-{variant}"), Some(&args)); if let Some(warning) = sent.warning { // Fumi's own warning text, appended as a second, independent // clause rather than folded into the catalogue — it isn't ours @@ -1112,12 +1113,11 @@ mod tests { #[test] fn interpolated_messages_carry_no_bidi_isolation_marks() { let mut args = fluent_bundle::FluentArgs::new(); - args.set("variant", "plain"); args.set("total", 5i64); args.set("stored", 5i64); args.set("rejected", 0i64); assert_eq!( - i18n::t_args("fetch-result-note", Some(&args)), + i18n::t_args("fetch-result-note-plain", Some(&args)), "5 fetched, 5 stored" ); } diff --git a/src/view.rs b/src/view.rs index 3bddc89..b14ba34 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1810,11 +1810,13 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender = msg .headers From e2795cfef9f4cc84dadbc0aa2f3466f091e08e8a Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 22:58:45 +0300 Subject: [PATCH 29/37] chore: bump version to 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- data/com.randogoth.carrier.metainfo.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 411e8bd..63cf783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ [[package]] name = "carrier" -version = "0.1.0" +version = "0.2.0" dependencies = [ "fluent-bundle", "fumi-core", diff --git a/Cargo.toml b/Cargo.toml index 6aa56dd..fdebc4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "carrier" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "A minimalist smolmail client built on fumi-core, with a Relm4 UI" license = "Apache-2.0" diff --git a/data/com.randogoth.carrier.metainfo.xml b/data/com.randogoth.carrier.metainfo.xml index 1ccb0e1..868afaa 100644 --- a/data/com.randogoth.carrier.metainfo.xml +++ b/data/com.randogoth.carrier.metainfo.xml @@ -14,6 +14,7 @@ com.randogoth.carrier.desktop + From 4360e452334942a492c9426aaa644c74624735c9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 9 Oct 2026 07:04:47 +0300 Subject: [PATCH 30/37] feat: add windows runner CI and release jobs --- .forgejo/workflows/ci.yml | 27 +++++++++++++++ .forgejo/workflows/release.yml | 63 ++++++++++++++++++++++++++++++++++ windows/bundle.sh | 38 ++++++++++++++++++++ windows/setup.sh | 29 ++-------------- 4 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 windows/bundle.sh diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index cd0dd1a..7a9c576 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -66,3 +66,30 @@ jobs: working-directory: carrier env: GITHUB_TOKEN: "" + + windows: + runs-on: windows-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + ref: main + path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-windows-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-windows- + # No devbox on Windows: the runner host itself is the MSYS2 MINGW64 + # toolchain that windows/setup.sh installs, so this runs the same + # commands devbox's `check` script runs on the other two platforms + # straight from cargo. + - run: cargo fmt --check && cargo build && cargo test + working-directory: carrier + shell: bash diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 90d2cc2..95437b8 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -73,6 +73,69 @@ jobs: "$FORGEJO/api/v1/repos/$REPO/releases/$release_id/assets?name=$NAME" working-directory: carrier + # Same non-gating strategy as the macos job above: this runner is a + # desktop machine that isn't guaranteed to be on when a tag is pushed, + # so it must never gate the release (and must not be gated by it + # either). It builds and bundles via windows/bundle.sh - the same + # script windows/setup.sh runs for local one-command builds - then + # appends the zip to the release the linux+flatpak jobs create, with + # the same poll-for-the-release the macos job does. + windows: + runs-on: windows-x86_64 + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + path: carrier + - uses: https://code.forgejo.org/actions/checkout@v4 + with: + repository: randogoth/fumi + ref: main + path: fumi + - uses: https://code.forgejo.org/actions/cache@v4 + with: + path: | + ~/.cargo/registry + carrier/target + fumi/target + key: cargo-windows-${{ hashFiles('carrier/Cargo.lock') }} + restore-keys: cargo-windows- + - run: bash windows/bundle.sh + working-directory: carrier + shell: bash + - run: | + REPO="${{ forge.repository }}" + TAG="${{ forge.ref_name }}" + FORGEJO="${{ env.FORGEJO_SERVER_URL }}" + FORGEJO="${FORGEJO%%/}" + TOKEN="${{ forge.token }}" + VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) + FILE="carrier-windows-$VERSION.zip" + NAME="carrier-windows-$VERSION.zip" + + release_id="" + for i in $(seq 1 40); do + body=$(curl --retry 5 -sS -H "Authorization: token $TOKEN" "$FORGEJO/api/v1/repos/$REPO/releases/tags/$TAG") + release_id=$(echo "$body" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') + if [ -n "$release_id" ]; then + break + fi + echo "release for $TAG not ready yet, waiting 15s ($i/40)" + sleep 15 + done + + if [ -z "$release_id" ]; then + echo "release for $TAG never appeared after 10 minutes" >&2 + exit 1 + fi + + curl --retry 5 --fail -sS -X POST \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@$FILE" \ + "$FORGEJO/api/v1/repos/$REPO/releases/$release_id/assets?name=$NAME" + working-directory: carrier + shell: bash + linux: runs-on: linux-x86_64 steps: diff --git a/windows/bundle.sh b/windows/bundle.sh new file mode 100644 index 0000000..c8ac496 --- /dev/null +++ b/windows/bundle.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Build carrier and bundle the result: carrier.exe plus the GTK runtime +# into dist/, then dist/ into carrier-windows-.zip. +# +# Run from the carrier checkout inside the MSYS2 MINGW64 shell (or with +# the MINGW64 toolchain on PATH — windows/setup.sh installs the deps, +# and the forgejo Windows runner host already carries them). +set -euo pipefail + +say() { printf '\n==> %s\n' "$*"; } +die() { printf '\n!! %s\n' "$*" >&2; exit 1; } + +say "building carrier (release; the first build takes a while)" +cargo build --release --locked || die "cargo build failed" + +say "bundling the runtime" +rm -rf dist +mkdir -p dist/share/glib-2.0/schemas \ + dist/share/icons/hicolor/512x512/apps \ + dist/share/icons/hicolor/256x256/apps \ + dist/share/icons/hicolor/128x128/apps +cp target/release/carrier.exe dist/ +for dll in $(ldd target/release/carrier.exe | awk '/mingw64/ {print $3}'); do + cp "$dll" dist/ || die "copying $dll failed" +done +[ "$(ls dist/*.dll | wc -l)" -gt 0 ] || die "no DLLs collected — ldd found none under /mingw64" +cp /mingw64/share/glib-2.0/schemas/* dist/share/glib-2.0/schemas/ +glib-compile-schemas dist/share/glib-2.0/schemas/ +cp -r /mingw64/share/icons/Adwaita dist/share/icons/ +cp -r /mingw64/share/icons/hicolor dist/share/icons/ +cp data/icons/com.randogoth.carrier.png dist/share/icons/hicolor/512x512/apps/ +cp data/icons/com.randogoth.carrier-256.png dist/share/icons/hicolor/256x256/apps/ +cp data/icons/com.randogoth.carrier-128.png dist/share/icons/hicolor/128x128/apps/ + +say "zipping" +version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) +rm -f "carrier-windows-$version.zip" +(cd dist && zip -qr "../carrier-windows-$version.zip" .) diff --git a/windows/setup.sh b/windows/setup.sh index fa8e780..f6a591d 100755 --- a/windows/setup.sh +++ b/windows/setup.sh @@ -5,6 +5,8 @@ # GTK runtime into dist/ and carrier-windows.zip. # # If MSYS2 updates its own core mid-run the shell may die — just rerun. +# Building and bundling live in windows/bundle.sh, shared with the +# forgejo Windows release job. set -euo pipefail say() { printf '\n==> %s\n' "$*"; } @@ -31,31 +33,6 @@ pacman -S --noconfirm --needed \ pkg-config --exists gtk4 libadwaita-1 \ || die "gtk4/libadwaita invisible to pkg-config — are you in the MINGW64 shell?" -say "building carrier (release; the first build takes a while)" -cargo build --release --locked || die "cargo build failed" - -say "bundling the runtime" -rm -rf dist -mkdir -p dist/share/glib-2.0/schemas \ - dist/share/icons/hicolor/512x512/apps \ - dist/share/icons/hicolor/256x256/apps \ - dist/share/icons/hicolor/128x128/apps -cp target/release/carrier.exe dist/ -for dll in $(ldd target/release/carrier.exe | awk '/mingw64/ {print $3}'); do - cp "$dll" dist/ || die "copying $dll failed" -done -[ "$(ls dist/*.dll | wc -l)" -gt 0 ] || die "no DLLs collected — ldd found none under /mingw64" -cp /mingw64/share/glib-2.0/schemas/* dist/share/glib-2.0/schemas/ -glib-compile-schemas dist/share/glib-2.0/schemas/ -cp -r /mingw64/share/icons/Adwaita dist/share/icons/ -cp -r /mingw64/share/icons/hicolor dist/share/icons/ -cp data/icons/com.randogoth.carrier.png dist/share/icons/hicolor/512x512/apps/ -cp data/icons/com.randogoth.carrier-256.png dist/share/icons/hicolor/256x256/apps/ -cp data/icons/com.randogoth.carrier-128.png dist/share/icons/hicolor/128x128/apps/ - -say "zipping" -version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) -rm -f "carrier-windows-$version.zip" -(cd dist && zip -qr "../carrier-windows-$version.zip" .) +bash windows/bundle.sh say "done — test with ./dist/carrier.exe (SmartScreen may ask once)" From 6bb850b9afb7dc6692bfea4e3ef324edb8b3643d Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 9 Oct 2026 10:42:50 +0300 Subject: [PATCH 31/37] fix: no console window and a writable config dir on Windows --- src/main.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6b112e9..2284343 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,10 @@ //! master, 0600), `carrier.db` (fumi-core's sealed store) and `read.txt` //! (read marks, carrier's own sidecar). +// No console window when the Windows release build is double-clicked. +// Debug builds keep it for eprintln output. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + mod app; #[cfg(debug_assertions)] mod demo; @@ -26,9 +30,7 @@ fn main() { fn config_dir() -> PathBuf { let base = match std::env::var("XDG_CONFIG_HOME") { Ok(xdg) if !xdg.is_empty() => PathBuf::from(xdg).join("carrier"), - _ => PathBuf::from(std::env::var("HOME").unwrap_or_default()) - .join(".config") - .join("carrier"), + _ => default_config_root(), }; // Dev builds get their own profile, seeded with sample data in // `demo.rs`, so layout work never touches the real account. @@ -36,3 +38,20 @@ fn config_dir() -> PathBuf { let base = base.with_file_name("carrier-demo"); base } + +// HOME is rarely set on Windows, and when it is missing the Unix +// fallback would resolve .config\carrier against the working +// directory - for an installed copy that's Program Files, which isn't +// writable, so the app would die on startup. Roaming AppData is the +// per-user config location there. +#[cfg(windows)] +fn default_config_root() -> PathBuf { + PathBuf::from(std::env::var("APPDATA").unwrap_or_default()).join("carrier") +} + +#[cfg(not(windows))] +fn default_config_root() -> PathBuf { + PathBuf::from(std::env::var("HOME").unwrap_or_default()) + .join(".config") + .join("carrier") +} From 433573693c3359d9b7dde23cbe768112043ef3ca Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 9 Oct 2026 10:42:50 +0300 Subject: [PATCH 32/37] feat: ship an Inno Setup installer from the windows release job --- .forgejo/workflows/release.yml | 26 +++++++++++++-------- .gitignore | 1 + windows/carrier.iss | 42 ++++++++++++++++++++++++++++++++++ windows/installer.sh | 31 +++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 windows/carrier.iss create mode 100644 windows/installer.sh diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 95437b8..feb7796 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -77,9 +77,11 @@ jobs: # desktop machine that isn't guaranteed to be on when a tag is pushed, # so it must never gate the release (and must not be gated by it # either). It builds and bundles via windows/bundle.sh - the same - # script windows/setup.sh runs for local one-command builds - then - # appends the zip to the release the linux+flatpak jobs create, with - # the same poll-for-the-release the macos job does. + # script windows/setup.sh runs for local one-command builds - wraps + # the bundle in an Inno Setup installer (windows/installer.sh; Inno + # lives in this runner host's user profile), then appends both the + # portable zip and the setup.exe to the release the linux+flatpak jobs + # create, with the same poll-for-the-release the macos job does. windows: runs-on: windows-x86_64 steps: @@ -102,6 +104,9 @@ jobs: - run: bash windows/bundle.sh working-directory: carrier shell: bash + - run: bash windows/installer.sh + working-directory: carrier + shell: bash - run: | REPO="${{ forge.repository }}" TAG="${{ forge.ref_name }}" @@ -109,8 +114,7 @@ jobs: FORGEJO="${FORGEJO%%/}" TOKEN="${{ forge.token }}" VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) - FILE="carrier-windows-$VERSION.zip" - NAME="carrier-windows-$VERSION.zip" + FILES="carrier-windows-$VERSION.zip carrier-windows-$VERSION-setup.exe" release_id="" for i in $(seq 1 40); do @@ -128,11 +132,13 @@ jobs: exit 1 fi - curl --retry 5 --fail -sS -X POST \ - -H "Authorization: token $TOKEN" \ - -H "Content-Type: multipart/form-data" \ - -F "attachment=@$FILE" \ - "$FORGEJO/api/v1/repos/$REPO/releases/$release_id/assets?name=$NAME" + for FILE in $FILES; do + curl --retry 5 --fail -sS -X POST \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@$FILE" \ + "$FORGEJO/api/v1/repos/$REPO/releases/$release_id/assets?name=$FILE" + done working-directory: carrier shell: bash diff --git a/.gitignore b/.gitignore index 6bc4627..51989d3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ result-* *.db-shm /dist carrier-windows-*.zip +carrier-windows-*-setup.exe diff --git a/windows/carrier.iss b/windows/carrier.iss new file mode 100644 index 0000000..e9fc5cb --- /dev/null +++ b/windows/carrier.iss @@ -0,0 +1,42 @@ +; carrier Windows installer. Packs the same portable bundle windows/ +; bundle.sh produces (dist/) into a classic setup.exe: Program Files, +; Start Menu shortcut, optional desktop icon, Apps & Features uninstall +; entry. Built by windows/installer.sh, which passes /DVersion= from +; Cargo.toml. +; +; Not yet wired into the forgejo release job - deliberately local until +; the experience is signed off. + +#ifndef Version + #error Pass the version: ISCC /DVersion=0.2.0 windows/carrier.iss +#endif + +[Setup] +AppId={{345BADA4-146B-4F57-B7D3-E9FD5D664F4B} +AppName=carrier +AppVersion={#Version} +AppPublisher=randogoth +DefaultDirName={autopf}\carrier +DefaultGroupName=carrier +; Next to the portable zip (paths relative to this script's directory). +OutputDir=.. +OutputBaseFilename=carrier-windows-{#Version}-setup +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64compatible +UninstallDisplayName=carrier + +[Tasks] +Name: "desktopicon"; Description: "Create a &desktop icon"; GroupDescription: "Additional icons:" + +[Files] +Source: "..\dist\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +; Shortcut icons come from carrier.exe's embedded .ico resource. +Name: "{group}\carrier"; Filename: "{app}\carrier.exe" +Name: "{autodesktop}\carrier"; Filename: "{app}\carrier.exe"; Tasks: desktopicon + +[Run] +Filename: "{app}\carrier.exe"; Description: "Launch carrier"; Flags: nowait postinstall skipifsilent diff --git a/windows/installer.sh b/windows/installer.sh new file mode 100644 index 0000000..906b8ef --- /dev/null +++ b/windows/installer.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Pack the dist/ bundle into carrier-windows--setup.exe. +# +# Run from the carrier checkout after windows/bundle.sh has produced +# dist/. Requires Inno Setup 7 (per-user install, which its own +# installer's "just me" option produces). +set -euo pipefail + +say() { printf '\n==> %s\n' "$*"; } +die() { printf '\n!! %s\n' "$*" >&2; exit 1; } + +# MSYS2's login shells prune LOCALAPPDATA from the environment, so fall +# back to cygpath (CSIDL 28 = local AppData of the current user). +if [ -n "${LOCALAPPDATA:-}" ]; then + appdata=$(cygpath -u "$LOCALAPPDATA") +else + appdata=$(cygpath -F 28) +fi +ISCC="$appdata/Programs/Inno Setup 7/ISCC.exe" +[ -f "$ISCC" ] || die "Inno Setup not found at $ISCC — install it from https://jrsoftware.org/isdl.php" + +[ -d dist ] || die "no dist/ bundle — run windows/bundle.sh first" + +version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1) + +say "compiling the installer" +# MSYS2_ARG_CONV_EXCL stops MSYS2 mangling /DVersion into a path. +MSYS2_ARG_CONV_EXCL="*" "$ISCC" /DVersion="$version" windows/carrier.iss \ + || die "ISCC failed" + +say "done — carrier-windows-$version-setup.exe (in the checkout root)" From 6172df3e0052b6f12bc2855fdbb5fa849ddd5439 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 8 Oct 2026 23:02:57 +0300 Subject: [PATCH 33/37] docs: add license, third-party notices, AI declaration, and a download section --- AI-DECLARATION.md | 12 + LICENSE | 201 ++++++ README.md | 73 +-- THIRD_PARTY_NOTICES.md | 1129 ++++++++++++++++++++++++++++++++++ docs/screenshots/carrier.png | Bin 0 -> 345504 bytes locales/en-US/main.ftl | 112 ++++ 6 files changed, 1485 insertions(+), 42 deletions(-) create mode 100644 AI-DECLARATION.md create mode 100644 LICENSE create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/screenshots/carrier.png diff --git a/AI-DECLARATION.md b/AI-DECLARATION.md new file mode 100644 index 0000000..1b5def6 --- /dev/null +++ b/AI-DECLARATION.md @@ -0,0 +1,12 @@ +--- +version: "0.1.2" +level: copilot +--- + +This format is based on [AI-DECLARATION.md](https://ai-declaration.md/en/0.1.2). + +## Notes + +- I directed the work step by step, reviewed and committed the results, and verified the UI live; build, packaging and cross-platform scripts. +- Mistral Vibe acted on a migration from Iced to Replm4, the follow-up refinement sessions, and the Flatpak and MSYS2 packaging. +- Claude Code was used throughout, prompted and reviewed by me at each step. Its main task was GTK4/adwaita refinements. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c98d27d --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 82198b3..e38ac4c 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,39 @@ # carrier -A minimalist desktop client for [Smol Mail](../smolmail): one compact window — the list above, the article below — built on [fumi-core](../fumi), the reference CLI's own library, with a [Relm4](https://relm4.org) (GTK4) UI built from libadwaita's own widgets, so it follows the user's theme and accent colour. One 32-byte seed is the only secret; mail stays sealed at rest and opens on demand. +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Built with Devbox](https://www.jetify.com/img/devbox/shield_galaxy.svg)](https://github.com/jetify-com/devbox/) [![AI-DECLARATION: copilot](https://img.shields.io/badge/䷼%20AI--DECLARATION-copilot-fee2e2?labelColor=fee2e2)](https://ai-declaration.md) + +A simple desktop client for [Smol Mail](../smolmail). One compact window: the message list above, the article below. It's built on [fumi-core](../fumi), the reference CLI's own library, with a [Relm4](https://relm4.org) (GTK4) UI made from libadwaita's own widgets, so it follows your theme and accent colour. One 32-byte seed is the only secret; mail stays sealed at rest and opens on demand. + +![carrier showing an inbox and an open message](docs/screenshots/carrier.png) ## What it does -- Create or restore an identity from a seed, shown once at creation time. -- Pin a server's key — paste the operator's, or connect and pin what it presents — then register a username or recall an existing address. -- Fetch (with cancel), read, reply and send sealed mail; the inbox, the requests tier and sent copies each keep their own folder. -- Composes closed by any sidebar trip are stashed as drafts — kept beside the store, reopened from a folder that appears only once one exists; Cancel is the only way to throw one away. -- Custom `Key: value` frontmatter rows when writing: one line per header under the subject, validated against the frontmatter rules — the client's own fields (subject, reply-to, in-reply-to, accept) can't be shadowed, and rows survive a stash as part of the draft. Received mail shows the sender's own fields in the message detail, between the meta line and the body; replying to such a mail asks whether to carry copies of the fields into the reply, where they stay editable. -- Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. -- Contacts with verified/tofu marks and smol:// import; each contact's detail names the server its address lives on and whether that server's key is pinned, with a pin action when it is not. Your own identity opens in the same detail view, its address in the header bar with a QR share beside it, key, fingerprint and URI below. -- Optional strict first contact: writing to someone new whose address carries no key means their key has to be looked up on their own server, and an unpinned server can answer with one of its own. With the setting on, that send stops and the compose offers to fetch and pin the server's key first, so it can be checked against what the operator published. A smol:// address whose key differs from the one already on file asks before replacing it, either way. -- Key rotation with a confirm-first step, a network timeout, gsmol backup export/import that never contains the seed, and a log-out that erases the local files and returns to onboarding. Preferences live in a plaintext sidecar beside the store, so they survive a restart. +- Create an identity from a seed, or restore one you already have. The seed shows once, at creation. +- Pin a server's key, either by pasting the operator's key or connecting and trusting what the server presents. Then register a username, or recall an address you already own. +- Fetch, read, reply to and send sealed mail. Your inbox, requests and sent mail each live in their own folder. +- Close a compose window from the sidebar and carrier saves it as a draft automatically. Drafts live beside your mail store, in a folder that shows up once you have one; Cancel is the only way to throw one away. +- Add your own `Key: value` headers when writing mail, one per line under the subject. carrier checks each line against the frontmatter rules and won't let you override its own fields (subject, reply-to, in-reply-to, accept). When you open mail with custom headers, they show up between the meta line and the body, and replying offers to carry them into your draft, where you can still edit them. +- Accept or block someone who writes to you with a self-certifying reply address. Accepting also attaches an accept token to that contact automatically. +- Import contacts from a smol:// link and see whether they're verified or just trust-on-first-use. Each contact's detail view names the server their address lives on, whether you've pinned that server's key, and a button to pin it if you haven't. Your own identity opens the same way, with a QR code next to your address in the header bar and your key, fingerprint and URI below. +- Turn on strict first contact to make carrier pause before writing to someone new whose address carries no key. With it on, carrier offers to fetch and pin the server's key first, so you can check it against what the operator published, before the send goes out. A smol:// address with a key that differs from the one on file asks before replacing it, strict mode or not. +- Rotate your key with a confirmation step and a network timeout. Export and import gsmol backups that never contain the seed. Log out to erase your local files and return to onboarding. Your preferences live in a small sidecar file next to the store, so they survive a restart. -## Layout +## Download -``` -src/main.rs config directory, demo seeding, launch -src/app.rs state and update; blocking calls run on the worker pool -src/view.rs the Relm4 component: widgets, the op adapter, the re-render -src/ops.rs fumi-core calls, key/read-mark files, and the offline tests +Each release carries its binaries on the forge: [downloads for carrier](https://code.randogoth.com/randogoth/carrier/releases). + +### Flatpak + +```bash +flatpak install --user carrier-x86_64.flatpak +flatpak run com.randogoth.carrier ``` -## State +### Windows -Everything lives under `$XDG_CONFIG_HOME/carrier` (default `~/.config/carrier`): `carrier.key` is the master seed, written 0600; `carrier.db` is fumi-core's sealed store; `read.txt` is carrier's own read marks, one hex id per line; `settings.txt` holds the preferences as one `key value` line each. +Download the [installer](https://code.randogoth.com/randogoth/carrier/releases/download/v0.2.0/carrier-windows-0.2.0-setup.exe) and run it -## Run and verify +## Build ``` devbox run run # build release and launch @@ -35,38 +41,21 @@ devbox run check # fmt --check, build, tests devbox run test # tests only ``` -The tests run offline against an in-memory store: seed and key parsing, a sealed envelope listed and opened end to end, contact import, a full backup round-trip, pin labels and the date formatter. `scripts/devbox-libs.sh` puts the GUI stack's runtime libraries (wayland, xkbcommon, EGL, vulkan) on the library path inside the devbox shell — they are dlopened, so nothing that links the binary can provide them. +Tests run offline against an in-memory store and cover seed and key parsing, listing and opening a sealed envelope end to end, importing a contact, a full backup round-trip, pin labels and the date formatter. `scripts/devbox-libs.sh` adds the GUI stack's runtime libraries (wayland, xkbcommon, EGL, vulkan) to the library path inside the devbox shell, since they're loaded at runtime and nothing that links the binary can provide them. -## Status +### Packaging -Work in progress: the UI is unexercised against a live server — the store, crypto and address paths are covered by tests, the network flows (register, fetch, send, rotate) are wired to fumi-core's operations but only smoke-tested as far as launching the window. - -## Packaging - -`devbox run package` builds and installs the app as a Flatpak from -`com.randogoth.carrier.json` — the binary, the desktop file, the metainfo and -the icon all come from the repo, and the tooling (`flatpak`, `flatpak-builder`, -`appstreamcli`) lives in the devbox environment. The flatpak runtime is the one -dependency outside devbox; install it once: +Run `devbox run package` to build carrier and install it as a Flatpak, using `com.randogoth.carrier.json`. It picks up the binary, desktop file, metainfo and icon from the repo, and devbox provides the tooling (`flatpak`, `flatpak-builder`, `appstreamcli`). The one thing outside devbox is the flatpak runtime itself; install it once: flatpak install --user flathub org.gnome.Sdk//51 org.freedesktop.Sdk.Extension.rust-stable//26.08 -The result is installed into the user flatpak and runs as -`flatpak run com.randogoth.carrier`. Inside the sandbox the app keeps its -config under `~/.var/app/com.randogoth.carrier/config/carrier`, so a flatpak -install starts with a fresh identity rather than the dev build's account. +After that, carrier lives in your user flatpak and runs with `flatpak run com.randogoth.carrier`. Inside the sandbox it keeps its config under `~/.var/app/com.randogoth.carrier/config/carrier`, so a flatpak install starts with a fresh identity rather than your dev build's account. ### Windows -Install MSYS2 (`winget install MSYS2.MSYS2`), open the **MSYS2 MINGW64** -shell, and run the one-command script from the carrier checkout: +Install MSYS2 (`winget install MSYS2.MSYS2`), open the **MSYS2 MINGW64** shell, and run the setup script from your carrier checkout: git clone ssh://git@code.randogoth.com:2222/randogoth/carrier.git cd carrier && windows/setup.sh -`windows/setup.sh` installs the GTK and Rust stack into MSYS2, clones fumi -next to carrier if it is missing, builds, and bundles carrier.exe with the -GTK runtime into `dist/` and a zip — the complete Windows distribution. The -app keeps its config in `%APPDATA%\carrier`. If MSYS2 updates its core -mid-run the shell may close; rerun the script and it continues. Cloning -over SSH needs the key in `~/.ssh` inside the MSYS2 home. +`windows/setup.sh` installs the GTK and Rust stack into MSYS2, clones fumi next to carrier if you don't already have it, builds carrier, and bundles carrier.exe with the GTK runtime into `dist/` and a zip, a complete Windows distribution. carrier keeps its config in `%APPDATA%\carrier`. If MSYS2 updates its core mid-run, the shell may close; just rerun the script and it picks up where it left off. Cloning over SSH needs your key in `~/.ssh` inside the MSYS2 home. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..62b6d1b --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,1129 @@ +# Third-Party Notices + +carrier incorporates the third-party components listed below. Components are grouped by license, each with the copyright notice that applies to it, followed by the full license text. A component listed under several licenses is multi-licensed under those options; any one of them may be chosen. All components are redistributed unmodified from their published sources. + +The Rust crates listed are all crates resolved by `Cargo.lock`, including transitive dependencies, which are statically linked into the carrier binary. +The SQLite C library is bundled in source form by the `libsqlite3-sys` crate and is public domain (see the final section). + +## Apache License 2.0 + +- aead 0.5.2 - Copyright (c) 2019 The RustCrypto Project Developers Copyright (c) 2019 MobileCoin, LLC +- aes 0.8.4 - Copyright (c) 2018 Artyom Pavlov +- aes-gcm 0.10.3 - Copyright (c) 2019 The RustCrypto Project Developers +- ahash 0.8.12 - Copyright (c) 2018 Tom Kaitchuck +- autocfg 1.5.1 - Copyright (c) 2018 Josh Stone +- base64ct 1.8.3 - Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) Copyright (c) 2021-2025 The RustCrypto Project Developers +- bitflags 2.13.2 - Copyright (c) 2014 The Rust Project Developers +- blake2 0.10.6 - Copyright (c) 2015-2016 The blake2-rfc Developers, Cesar Barros Copyright (c) 2017 Artyom Pavlov +- block-buffer 0.10.4 - Copyright (c) 2018-2019 The RustCrypto Project Developers +- bumpalo 3.20.3 - Copyright (c) 2019 Nick Fitzgerald +- cc 1.5.1 - Copyright (c) 2014 Alex Crichton +- cfg-expr 0.20.10 - Copyright (c) 2019 Embark Studios +- cfg-if 1.0.5 - Copyright (c) 2014 Alex Crichton +- chacha20 0.9.1 - Copyright (c) 2019-2023 The RustCrypto Project Developers +- chacha20poly1305 0.10.1 - Copyright (c) 2019 The RustCrypto Project Developers +- cipher 0.4.4 - Copyright (c) 2016-2020 RustCrypto Developers +- const-oid 0.9.6 - Copyright (c) 2020-2022 The RustCrypto Project Developers +- cpufeatures 0.2.17 - Copyright (c) 2020-2025 The RustCrypto Project Developers +- crypto-common 0.1.7 - Copyright (c) 2021 RustCrypto Developers +- ctr 0.9.2 - Copyright (c) 2018-2022 RustCrypto Developers Copyright (c) 2018 Artyom Pavlov +- curve25519-dalek-derive 0.1.1 - The curve25519-dalek-derive developers +- der 0.7.10 - Copyright (c) 2020-2023 The RustCrypto Project Developers +- digest 0.10.7 - Copyright (c) 2017 Artyom Pavlov +- displaydoc 0.2.7 - Jane Lusby +- ed25519 2.2.3 - Copyright 2018-2022 RustCrypto Developers +- equivalent 1.0.2 - Copyright (c) 2016--2023 +- errno 0.3.14 - Copyright (c) 2014 Chris Wong +- fallible-iterator 0.3.0 - Copyright (c) 2015 The rust-openssl-verify Developers +- fallible-streaming-iterator 0.1.9 - Copyright (c) 2016 The fallible-streaming-iterator Developers +- fastrand 2.5.0 - Stjepan Glavina +- fiat-crypto 0.2.9 - Copyright 2015-2020 the fiat-crypto authors (see the AUTHORS file) +- field-offset 0.3.6 - Copyright (c) 2016-2021 Diggory Blake, and other contributors. +- find-msvc-tools 0.1.14 - Copyright (c) 2014 Alex Crichton +- fluent-bundle 0.16.0 - Copyright 2017 Mozilla +- fluent-langneg 0.13.1 - Zibi Braniecki +- fluent-syntax 0.12.0 - Copyright 2017 Mozilla +- flume 0.11.1 - Joshua Barretto +- fragile 2.1.0 - Armin Ronacher +- futures 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-channel 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-core 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-executor 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-io 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-macro 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-sink 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-task 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-util 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- getrandom 0.2.17 - Copyright (c) 2018-2024 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers +- getrandom 0.4.3 - Copyright (c) 2018-2026 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers +- ghash 0.5.1 - Copyright (c) 2019 RustCrypto Developers +- hashbrown 0.14.5 - Copyright (c) 2016 Amanieu d'Antras +- hashbrown 0.17.1 - Copyright (c) 2016 Amanieu d'Antras +- hashlink 0.9.1 - kyren +- heck 0.5.0 - Copyright (c) 2015 The Rust Project Developers +- hkdf 0.12.4 - Copyright (c) 2015-2018 Vlad Filippov Copyright (c) 2018-2021 RustCrypto Developers +- hmac 0.12.1 - Copyright (c) 2017 Artyom Pavlov +- indexmap 2.14.2 - Copyright (c) 2016--2017 +- inout 0.1.4 - Copyright (c) 2022 The RustCrypto Project Developers Copyright (c) 2022 Artyom Pavlov +- intl-memoizer 0.5.3 - Copyright 2017 Mozilla +- intl_pluralrules 7.0.2 - Kekoa Riggin , Zibi Braniecki +- itoa 1.0.18 - David Tolnay +- js-sys 0.3.106 - Copyright (c) 2014 Alex Crichton +- libc 0.2.189 - Copyright (c) The Rust Project Developers +- linux-raw-sys 0.12.1 - Dan Gohman +- lock_api 0.4.14 - Copyright (c) 2016 The Rust Project Developers +- once_cell 1.21.4 - Aleksey Kladov +- opaque-debug 0.3.1 - Copyright (c) 2018-2024 The RustCrypto Project Developers +- pin-project-lite 0.2.17 - The pin-project-lite developers +- pkcs8 0.10.2 - Copyright (c) 2020-2023 The RustCrypto Project Developers +- pkg-config 0.3.34 - Copyright (c) 2014 Alex Crichton +- poly1305 0.8.0 - Copyright (c) 2015-2019 RustCrypto Developers +- polyval 0.6.2 - Copyright (c) 2019-2023 RustCrypto Developers +- proc-macro-crate 3.5.0 - Bastian Köcher +- proc-macro2 1.0.107 - David Tolnay , Alex Crichton +- quote 1.0.47 - David Tolnay +- r-efi 6.0.0 - The r-efi developers +- rand_core 0.6.4 - Copyright 2018 Developers of the Rand project Copyright (c) 2014 The Rust Project Developers +- relm4 0.9.1 - Aaron Erhardt +- relm4-css 0.9.0 - Aaron Erhardt +- relm4-macros 0.9.1 - Aaron Erhardt +- rustc-hash 2.1.3 - The Rust Project Developers +- rustc_version 0.4.1 - Copyright (c) 2016 The Rust Project Developers +- rustix 1.1.5 - Dan Gohman , Jakub Konka +- rustversion 1.0.23 - David Tolnay +- scopeguard 1.2.0 - Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers +- self_cell 1.3.0 - Lukas Bergdoll +- semver 1.0.28 - David Tolnay +- serde 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_core 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_derive 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_json 1.0.151 - Erick Tryzelaar , David Tolnay +- serde_spanned 1.1.1 - Copyright (c) Individual contributors +- sha2 0.10.9 - Copyright (c) 2006-2009 Graydon Hoare Copyright (c) 2009-2013 Mozilla Foundation Copyright (c) 2016 Artyom Pavlov +- shlex 2.0.1 - Copyright 2015 Nicholas Allegra (comex). +- signature 2.2.0 - Copyright (c) 2018-2023 RustCrypto Developers +- smallvec 1.16.2 - Copyright (c) 2018 The Servo Project Developers +- snow 0.9.6 - Copyright (c) 2021 Jake McGinty +- spki 0.7.3 - Copyright (c) 2021-2023 The RustCrypto Project Developers +- syn 2.0.119 - David Tolnay +- syn 3.0.6 - David Tolnay +- system-deps 7.0.8 - Guillaume Desmottes , Josh Triplett +- target-lexicon 0.13.5 - Dan Gohman +- tempfile 3.27.0 - Copyright (c) 2015 Steven Allen +- thiserror 2.0.21 - David Tolnay +- thiserror-impl 2.0.21 - David Tolnay +- toml 1.1.6+spec-1.1.0 - Copyright (c) Individual contributors +- toml_datetime 1.1.1+spec-1.1.0 - Copyright (c) Individual contributors +- toml_edit 0.25.15+spec-1.1.0 - Copyright (c) Individual contributors +- toml_parser 1.1.3+spec-1.1.0 - Copyright (c) Individual contributors +- toml_writer 1.1.2+spec-1.1.0 - Copyright (c) Individual contributors +- type-map 0.5.1 - Copyright 2017-NOW Actix Team +- typenum 1.20.1 - Copyright 2014 Paho Lurie-Gregg +- unic-langid 0.9.6 - Zibi Braniecki +- unic-langid-impl 0.9.6 - Zibi Braniecki +- unicode-ident 1.0.26 - David Tolnay +- universal-hash 0.5.1 - Copyright (c) 2019-2020 RustCrypto Developers +- vcpkg 0.2.15 - Copyright (c) 2017 Jim McGrath +- version_check 0.9.5 - Copyright (c) 2017-2018 Sergio Benitez +- wasi 0.11.1+wasi-snapshot-preview1 - The Cranelift Project Developers +- wasm-bindgen 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-macro 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-macro-support 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-shared 0.2.129 - Copyright (c) 2014 Alex Crichton +- windows-link 0.2.1 - The windows-link developers +- windows-sys 0.59.0 - Microsoft +- windows-sys 0.61.2 - The windows-sys developers +- windows-targets 0.52.6 - Microsoft +- windows_aarch64_gnullvm 0.52.6 - Microsoft +- windows_aarch64_msvc 0.52.6 - Microsoft +- windows_i686_gnu 0.52.6 - Microsoft +- windows_i686_gnullvm 0.52.6 - Microsoft +- windows_i686_msvc 0.52.6 - Microsoft +- windows_x86_64_gnu 0.52.6 - Microsoft +- windows_x86_64_gnullvm 0.52.6 - Microsoft +- windows_x86_64_msvc 0.52.6 - Microsoft +- zerocopy 0.8.59 - Copyright 2023 The Fuchsia Authors +- zerocopy-derive 0.8.59 - Copyright 2023 The Fuchsia Authors +- zeroize 1.9.0 - Copyright (c) 2018-2026 The RustCrypto Project Developers +- zeroize_derive 1.5.0 - Copyright (c) 2019-2026 The RustCrypto Project Developers + +``` +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +## MIT License + +- aead 0.5.2 - Copyright (c) 2019 The RustCrypto Project Developers Copyright (c) 2019 MobileCoin, LLC +- aes 0.8.4 - Copyright (c) 2018 Artyom Pavlov +- aes-gcm 0.10.3 - Copyright (c) 2019 The RustCrypto Project Developers +- ahash 0.8.12 - Copyright (c) 2018 Tom Kaitchuck +- autocfg 1.5.1 - Copyright (c) 2018 Josh Stone +- base64ct 1.8.3 - Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com) Copyright (c) 2021-2025 The RustCrypto Project Developers +- bitflags 2.13.2 - Copyright (c) 2014 The Rust Project Developers +- blake2 0.10.6 - Copyright (c) 2015-2016 The blake2-rfc Developers, Cesar Barros Copyright (c) 2017 Artyom Pavlov +- block-buffer 0.10.4 - Copyright (c) 2018-2019 The RustCrypto Project Developers +- bumpalo 3.20.3 - Copyright (c) 2019 Nick Fitzgerald +- cairo-rs 0.20.12 - The gtk-rs Project Developers +- cairo-sys-rs 0.20.10 - The gtk-rs Project Developers +- cc 1.5.1 - Copyright (c) 2014 Alex Crichton +- cfg-expr 0.20.10 - Copyright (c) 2019 Embark Studios +- cfg-if 1.0.5 - Copyright (c) 2014 Alex Crichton +- chacha20 0.9.1 - Copyright (c) 2019-2023 The RustCrypto Project Developers +- chacha20poly1305 0.10.1 - Copyright (c) 2019 The RustCrypto Project Developers +- cipher 0.4.4 - Copyright (c) 2016-2020 RustCrypto Developers +- const-oid 0.9.6 - Copyright (c) 2020-2022 The RustCrypto Project Developers +- cpufeatures 0.2.17 - Copyright (c) 2020-2025 The RustCrypto Project Developers +- crypto-common 0.1.7 - Copyright (c) 2021 RustCrypto Developers +- ctr 0.9.2 - Copyright (c) 2018-2022 RustCrypto Developers Copyright (c) 2018 Artyom Pavlov +- curve25519-dalek-derive 0.1.1 - The curve25519-dalek-derive developers +- data-encoding 2.11.1 - Copyright (c) 2015-2020 Julien Cretin Copyright (c) 2017-2020 Google Inc. +- der 0.7.10 - Copyright (c) 2020-2023 The RustCrypto Project Developers +- digest 0.10.7 - Copyright (c) 2017 Artyom Pavlov +- displaydoc 0.2.7 - Jane Lusby +- ed25519 2.2.3 - Copyright (c) 2018-2023 RustCrypto Developers +- equivalent 1.0.2 - Copyright (c) 2016--2023 +- errno 0.3.14 - Copyright (c) 2014 Chris Wong +- fallible-iterator 0.3.0 - Copyright (c) 2015 The rust-openssl-verify Developers +- fallible-streaming-iterator 0.1.9 - Copyright (c) 2016 The fallible-streaming-iterator Developers +- fastrand 2.5.0 - Stjepan Glavina +- fiat-crypto 0.2.9 - Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file). +- field-offset 0.3.6 - Copyright (c) 2016-2021 Diggory Blake, and other contributors. +- find-msvc-tools 0.1.14 - Copyright (c) 2014 Alex Crichton +- fluent-bundle 0.16.0 - Copyright 2017 Mozilla +- fluent-langneg 0.13.1 - Zibi Braniecki +- fluent-syntax 0.12.0 - Copyright 2017 Mozilla +- flume 0.11.1 - Joshua Barretto +- futures 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-channel 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-core 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-executor 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-io 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-macro 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-sink 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-task 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- futures-util 0.3.34 - Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors +- gdk-pixbuf 0.20.10 - The gtk-rs Project Developers +- gdk-pixbuf-sys 0.20.10 - The gtk-rs Project Developers +- gdk4 0.9.6 - The gtk-rs Project Developers +- gdk4-sys 0.9.6 - The gtk-rs Project Developers +- generic-array 0.14.7 - Copyright (c) 2015 Bartłomiej Kamiński +- getrandom 0.2.17 - Copyright (c) 2018-2024 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers +- getrandom 0.4.3 - Copyright (c) 2018-2026 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers +- ghash 0.5.1 - Copyright (c) 2019 RustCrypto Developers +- gio 0.20.12 - The gtk-rs Project Developers +- gio-sys 0.20.10 - The gtk-rs Project Developers +- glib 0.20.12 - The gtk-rs Project Developers +- glib-macros 0.20.12 - The gtk-rs Project Developers +- glib-sys 0.20.10 - The gtk-rs Project Developers +- gobject-sys 0.20.10 - The gtk-rs Project Developers +- graphene-rs 0.20.10 - The gtk-rs Project Developers +- graphene-sys 0.20.10 - The gtk-rs Project Developers +- gsk4 0.9.6 - The gtk-rs Project Developers +- gsk4-sys 0.9.6 - The gtk-rs Project Developers +- gtk4 0.9.7 - The gtk-rs Project Developers +- gtk4-macros 0.9.5 - The gtk-rs Project Developers +- gtk4-sys 0.9.6 - The gtk-rs Project Developers +- hashbrown 0.14.5 - Copyright (c) 2016 Amanieu d'Antras +- hashbrown 0.17.1 - Copyright (c) 2016 Amanieu d'Antras +- hashlink 0.9.1 - kyren +- heck 0.5.0 - Copyright (c) 2015 The Rust Project Developers +- hkdf 0.12.4 - Copyright (c) 2015-2018 Vlad Filippov Copyright (c) 2018-2021 RustCrypto Developers +- hmac 0.12.1 - Copyright (c) 2017 Artyom Pavlov +- indexmap 2.14.2 - Copyright (c) 2016--2017 +- inout 0.1.4 - Copyright (c) 2022 The RustCrypto Project Developers Copyright (c) 2022 Artyom Pavlov +- intl-memoizer 0.5.3 - Copyright 2017 Mozilla +- intl_pluralrules 7.0.2 - Kekoa Riggin , Zibi Braniecki +- itoa 1.0.18 - David Tolnay +- js-sys 0.3.106 - Copyright (c) 2014 Alex Crichton +- libadwaita 0.7.2 - Bilal Elmoussaoui +- libadwaita-sys 0.7.2 - Bilal Elmoussaoui +- libc 0.2.189 - Copyright (c) The Rust Project Developers +- libsqlite3-sys 0.30.1 - Copyright (c) 2014-2021 The rusqlite developers +- linux-raw-sys 0.12.1 - Dan Gohman +- lock_api 0.4.14 - Copyright (c) 2016 The Rust Project Developers +- memchr 2.8.3 - Copyright (c) 2015 Andrew Gallant +- memoffset 0.9.1 - Copyright (c) 2017 Gilad Naaman +- once_cell 1.21.4 - Aleksey Kladov +- opaque-debug 0.3.1 - Copyright (c) 2018-2024 The RustCrypto Project Developers +- pango 0.20.12 - The gtk-rs Project Developers +- pango-sys 0.20.10 - The gtk-rs Project Developers +- pin-project-lite 0.2.17 - The pin-project-lite developers +- pkcs8 0.10.2 - Copyright (c) 2020-2023 The RustCrypto Project Developers +- pkg-config 0.3.34 - Copyright (c) 2014 Alex Crichton +- poly1305 0.8.0 - Copyright (c) 2015-2019 RustCrypto Developers +- polyval 0.6.2 - Copyright (c) 2019-2023 RustCrypto Developers +- proc-macro-crate 3.5.0 - Bastian Köcher +- proc-macro2 1.0.107 - David Tolnay , Alex Crichton +- qrcodegen 1.8.0 - Project Nayuki +- quote 1.0.47 - David Tolnay +- r-efi 6.0.0 - The r-efi developers +- rand_core 0.6.4 - Copyright 2018 Developers of the Rand project Copyright (c) 2014 The Rust Project Developers +- relm4 0.9.1 - Aaron Erhardt +- relm4-css 0.9.0 - Aaron Erhardt +- relm4-macros 0.9.1 - Aaron Erhardt +- rusqlite 0.32.1 - Copyright (c) 2014-2021 The rusqlite developers +- rustc-hash 2.1.3 - The Rust Project Developers +- rustc_version 0.4.1 - Copyright (c) 2016 The Rust Project Developers +- rustix 1.1.5 - Dan Gohman , Jakub Konka +- rustversion 1.0.23 - David Tolnay +- scopeguard 1.2.0 - Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers +- semver 1.0.28 - David Tolnay +- serde 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_core 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_derive 1.0.229 - Erick Tryzelaar , David Tolnay +- serde_json 1.0.151 - Erick Tryzelaar , David Tolnay +- serde_spanned 1.1.1 - Copyright (c) Individual contributors +- sha2 0.10.9 - Copyright (c) 2006-2009 Graydon Hoare Copyright (c) 2009-2013 Mozilla Foundation Copyright (c) 2016 Artyom Pavlov +- shlex 2.0.1 - Copyright (c) 2015 Nicholas Allegra (comex). +- signature 2.2.0 - Copyright (c) 2018-2023 RustCrypto Developers +- slab 0.4.12 - Copyright (c) 2019 Carl Lerche +- smallvec 1.16.2 - Copyright (c) 2018 The Servo Project Developers +- snow 0.9.6 - Copyright (c) 2021 Jake McGinty +- spin 0.9.9 - Copyright (c) 2014 Mathijs van de Nes +- spki 0.7.3 - Copyright (c) 2021-2023 The RustCrypto Project Developers +- syn 2.0.119 - David Tolnay +- syn 3.0.6 - David Tolnay +- system-deps 7.0.8 - Guillaume Desmottes , Josh Triplett +- tempfile 3.27.0 - Copyright (c) 2015 Steven Allen +- thiserror 2.0.21 - David Tolnay +- thiserror-impl 2.0.21 - David Tolnay +- tokio 1.53.1 - Copyright (c) Tokio Contributors +- toml 1.1.6+spec-1.1.0 - Copyright (c) Individual contributors +- toml_datetime 1.1.1+spec-1.1.0 - Copyright (c) Individual contributors +- toml_edit 0.25.15+spec-1.1.0 - Copyright (c) Individual contributors +- toml_parser 1.1.3+spec-1.1.0 - Copyright (c) Individual contributors +- toml_writer 1.1.2+spec-1.1.0 - Copyright (c) Individual contributors +- tracing 0.1.44 - Copyright (c) 2019 Tokio Contributors +- tracing-attributes 0.1.31 - Copyright (c) 2019 Tokio Contributors +- tracing-core 0.1.36 - Copyright (c) 2019 Tokio Contributors +- type-map 0.5.1 - Copyright (c) 2022 Jacob Brown +- typenum 1.20.1 - Copyright (c) 2014 Paho Lurie-Gregg +- unic-langid 0.9.6 - Zibi Braniecki +- unic-langid-impl 0.9.6 - Zibi Braniecki +- unicode-ident 1.0.26 - David Tolnay +- universal-hash 0.5.1 - Copyright (c) 2019-2020 RustCrypto Developers +- vcpkg 0.2.15 - Copyright (c) 2017 Jim McGrath +- version-compare 0.2.1 - Copyright (c) 2017 Tim Visée +- version_check 0.9.5 - Copyright (c) 2017-2018 Sergio Benitez +- wasi 0.11.1+wasi-snapshot-preview1 - The Cranelift Project Developers +- wasm-bindgen 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-macro 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-macro-support 0.2.129 - Copyright (c) 2014 Alex Crichton +- wasm-bindgen-shared 0.2.129 - Copyright (c) 2014 Alex Crichton +- windows-link 0.2.1 - The windows-link developers +- windows-sys 0.59.0 - Microsoft +- windows-sys 0.61.2 - The windows-sys developers +- windows-targets 0.52.6 - Microsoft +- windows_aarch64_gnullvm 0.52.6 - Microsoft +- windows_aarch64_msvc 0.52.6 - Microsoft +- windows_i686_gnu 0.52.6 - Microsoft +- windows_i686_gnullvm 0.52.6 - Microsoft +- windows_i686_msvc 0.52.6 - Microsoft +- windows_x86_64_gnu 0.52.6 - Microsoft +- windows_x86_64_gnullvm 0.52.6 - Microsoft +- windows_x86_64_msvc 0.52.6 - Microsoft +- winnow 1.0.4 - The winnow developers +- winresource 0.1.31 - Copyright 2016 Max Resch +- zerocopy 0.8.59 - Copyright 2023 The Fuchsia Authors +- zerocopy-derive 0.8.59 - Copyright 2023 The Fuchsia Authors +- zeroize 1.9.0 - Copyright (c) 2018-2026 The RustCrypto Project Developers +- zeroize_derive 1.5.0 - Copyright (c) 2019-2026 The RustCrypto Project Developers +- zmij 1.0.23 - David Tolnay + +``` +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. +``` + +## BSD 3-Clause License + +- curve25519-dalek 4.1.3 - Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved. Copyright (c) 2016-2021 Henry de Valence. All rights reserved. +- ed25519-dalek 2.2.0 - Copyright (c) 2017-2019 isis agora lovecruft. All rights reserved. +- subtle 2.6.1 - Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. +- x25519-dalek 2.0.1 - Copyright (c) 2017-2021 isis agora lovecruft. All rights reserved. Copyright (c) 2019-2021 DebugSteven. All rights reserved. + +``` +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +Portions of curve25519-dalek were originally derived from Adam Langley's +Go ed25519 implementation, found at , +under the following licence: + +======================================================================== + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +## BSD 2-Clause License + +- zerocopy 0.8.59 - Copyright 2019 The Fuchsia Authors. +- zerocopy-derive 0.8.59 - Copyright 2019 The Fuchsia Authors. + +``` +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +## BSD 1-Clause License + +- fiat-crypto 0.2.9 - Copyright (c) 2015-2020 the fiat-crypto authors (see the AUTHORS file) All rights reserved. + +``` +The BSD 1-Clause License (BSD-1-Clause) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, +Inc. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +## The Unlicense + +- memchr 2.8.3 - Andrew Gallant , bluss + +``` +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to +``` + +## Unicode License v3 + +- tinystr 0.8.4 (Unicode license applies in addition to MIT OR Apache-2.0) - The ICU4X Project Developers +- unicode-ident 1.0.26 (Unicode license applies in addition to MIT OR Apache-2.0) - Copyright © 1991-2023 Unicode, Inc. +- zerofrom 0.1.8 (Unicode license applies in addition to MIT OR Apache-2.0) - The ICU4X Project Developers +- zerovec 0.11.8 (Unicode license applies in addition to MIT OR Apache-2.0) - The ICU4X Project Developers + +``` +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. +``` + +## LLVM Exception (to the Apache License 2.0) + +The component above may additionally be used under Apache-2.0 or MIT; when used under Apache-2.0, the license text above is modified by the following exception: + +- linux-raw-sys 0.12.1 (when used under Apache-2.0) - Dan Gohman +- rustix 1.1.5 (when used under Apache-2.0) - Dan Gohman , Jakub Konka +- target-lexicon 0.13.5 (when used under Apache-2.0) - Dan Gohman +- wasi 0.11.1+wasi-snapshot-preview1 (when used under Apache-2.0) - The Cranelift Project Developers + +``` +---- LLVM Exceptions to the Apache 2.0 License ---- + + As an exception, if, as a result of your compiling your source code, portions + of this Software are embedded into an Object form of such source code, you + may redistribute such embedded portions in such Object form without complying + with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + + In addition, if you combine or link compiled forms of this Software with + software that is licensed under the GPLv2 ("Combined Software") and if a + court of competent jurisdiction determines that the patent provision (Section + 3), the indemnity provision (Section 9) or other Section of the License + conflicts with the conditions of the GPLv2, you may retroactively and + prospectively choose to deem waived or otherwise exclude such Section(s) of + the License, but only in their entirety and only with respect to the Combined + Software. +``` + +## Zlib License + +- nanorand 0.7.0 - Lucy + +``` +zlib License + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. +``` + +## GNU LGPL v2.1 or later + +- r-efi 6.0.0 - The r-efi developers + +``` +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +GNU LESSER GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! +``` + +## GNU GPL v2 only + +- self_cell 1.3.0 - Lukas Bergdoll + +``` +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice +``` + +## SQLite (public domain) + +SQLite 3.46.0 is bundled in source form by the `libsqlite3-sys` crate (the crate itself is listed with the crates above). The SQLite authors have placed the code in the public domain; the notice accompanying the SQLite distribution: + +``` +The author disclaims copyright to the source code. Although no +laws apply, the author would appreciate it if you did not remove +this notice and attributed the source to the SQLite project. +``` diff --git a/docs/screenshots/carrier.png b/docs/screenshots/carrier.png new file mode 100644 index 0000000000000000000000000000000000000000..7509353ac30924bcd2c46d3c0c77194707ecf7c1 GIT binary patch literal 345504 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

a8(n%H7&Cqi#AF#K8O#RBg=JY0=naez< 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%0A`;V!u?b_B>R$VJf8}D} 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

  • qadww*d-0v! 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%0D$&?2rRu4b_T=SttgR5Zo-43_Kpli%TxSEfq zo~{1&@^z7g%#0Z`8dj|e6J4HRlJ)lcj~^B+juKlUvXo}}M1}gdU7dXGnwf!_nCzbS zzb7=hwufJAhm`I&?{y8AD^hxRc*IyTEq@6aKei{_{bLYxt^L=z|n;O-0 zkRfSfAw$EW8M>2gb{i1BQhRCO|>Wv;MJfT|` zUd(V1I1^Tw7Of^&UG15&IWlCm>FTJd%Q8Kd28G>tR#34rW}O1~!l%P&n-%5y55NB& z{W&Raz4)wUiRtO=H*@k**33Gq)^${=>EME`GcM+BUwr*FuhtaH2Vbi$zAf9ZcW-ly z-tRfjcCL9|^UL=Ad?U!6JTEr#s9VmvD*pN5k#nCv>F90aXEvVecl=fV>$2T~eC-$V zwy%x5`*m03wQFyl=N-RuN9NqzYTmbH(l57Iau)yklBuIRQ~ONY)0XPJ_fDu>{*Zpy zKJDx5i^*0zu`+W1o3$+2yp{^-#qDV^PCpm1fBsC*2kQ1eKQ3t5(igV6)%^Y+zJ1^K zmVZ2@o&NA^)x`{x4Lf&s-rk-c{W~wO{igg)9#KhUVVCypzZ0UX`b9!Ax8K$f>)uhE zF1iqWn00e(7Mh*=EmgOa30}WZGZ!-(7P5Zu#$Z$M;9*Jo@(5x|#C^pG%;N zk{~-niqY=^tGOCt-4?aKMDEpm_C4~L(?aIk#Ouc&J~ui2>D$G(eBU1@$lu8m7ZVdR zu(t>0u8u_&8Nc1XZ@!rE;MUgH5s`-8jI$FrNt~N!+Z>?LQ?ebLUZ{943%eN`$ z5p_NIecyMzd$r$pM?^&(0iD_Sa(VrM^Y#C_tAB+O>B<=u17l-m&q){Ge-G3U*|2^3k-65Z zmBq{3x%H)b)n0vN`}Y02AP38bTiNR!rip&3iVaz9x@PU##NuM^r$v%?^TeAJ7R2Z+ zj#?WfD$CT!P_$EK(n%4kxqP7WSl`?*n&YQF>#Q1AE7P@WW}quu?Cslwm-{&hoSAUu z>gw=~yLNSjd2c@VH)y3u)LJ!@wY@7>dg|!w^E$Dy-OAnUdimTry=&LaFT5^&_?>;j zo;^IFrrJF-rYdS(D3zUg#_m_G?8X}wOpOd-VbegzM~QU1#_TSW-5bZx&HXtdGO|s@ z^F_%ShQPgX$FE&8`%`!SMCG4{N9DeAADh7)^~?R=zYk6mAD(af96D>+!VoRRxn|6+ z%l+my-CgH&{p?Yd=;+%Kv9VkGj(+&@{Ig}s=HC+X^ACV79XLIGeL`}w?9Cj$?Rj@y zmIfXB^G62MqAjvq`1;tw$D7*b&YkP5yX4F@->yXt(?nO-p8Zg>|7OmkTU%dih;>KA z#2orkB9pg0dxI{oNVn^nb?c&XSC+(XzjZ{W=l0&uENzJjQ@s|(tq0YzmoHzoD1K(* z64)Pa~wPAvM?Q4%$+}Q~_S$XBkmCm}8=FFJsDf4qj`1-iR|Ni|u@mqhov9t5v zI(g8A+p}jkpG|9CH@mxJ+qP{R_U^Ua5r2Ev{8g(s+7EyD{kLG}9EOI@PR=7q8>{C< zUp9I3R+aINo85f&0)y*YckOB`+S#;psjG{Upo$P@$D)R^+d1#IZQIuNHg|K-g~@Z4 zo7ii|IV(J9UN!5Xvgf1(3ARHI1)2^rOq)KP?dsZZfqK)|Oe<`VWZ6^lQV5jV&CR=K z&ODOva8hH|)~*zz*4x|j`B#P8?tE*;^Lg)<-MgFpmiOMg8R_Heo0OL))|S{;YBci* z=m0Y_GxsQUUgy;7>tfv(1{`^`DAM%IpFLC8d_Vs0pIuq`+4IX2Z*OCl)SEYJrsspT z+waN!$=Txi_tdFPru(O#Oxdt)TVK+~mb&{n)!R33?k?Ni8@9Uj_ulp{j7qg?oX<PZCdOEwfew-8d z_HxiwNMF8){Cji0uBG^Sob&qYhp$`_S+-1V;VP}Jqe|PhZQHnSpP@$lt?gb~{PU0V ztmE;w;P>Aoo3q_A&Ana!U77^jsi#JDzi;1nnx^{WZ`$lN0U9me@BNnRlZ|#c`!`H@;i{JN)6|(eCLFK38tsvxg^8M39U1bNSwsSuYAK zW?B%Ub@=&br)f_oUOL)-U;br@RCV?46DdXoyYGh0%nIGQFlgn2TU)bl+`CtqV5+^j zevdThoay=V+s{8geCUu()>f@^^X=`6c)*tkzBqa%!=&KpDNzZw3mI1wJ&k1etha63 zmJqhWME}Y(uT+!SXD5EW&z_Z0qX1dRbeg zR$X0nwQhOX>W8nct`=~;m^IOQO$Ow~^n#a{R6QoK_-02cs>EJrPi&T&<99q^f2oO7 z@4-dV%&S)M9TMbyedo)ywb2`Hzg=*EqYiJ)Hs7#!FSBdl zgtN~AR)$=B{~biW{>m3_xyf1Uw6D2atqEDoJyO_6qqR+B~&6>^U#W#lrZjIXbtg7$U{1Pi(-^&k5 ztk{zE_*NY)30Q9)ur_RA<}Ky3D|z2<-@7+C^k~(EZRS$EQ@ox|WKBv_*qjr(w#-7N zxz=dS2d}MBvRi!Q@2-D%=-6-D`9J?ppY!!*%I2Bxr#^hWDQfMan>lhnCC;V^?~LKw z8z;Vd_wH*pI}=~LC_CF_zcuRYFP}QO##!v4r|T{nKyT}u;r^xKu6eytYx_d9 zdW&{`x-x(D)k~lPH^YQ$|9$q^XF<1)@Z0@35cM-)Wyr?u+uQH||CfAX6UzaKE&5!_ zGJK0~zg-lxQfdBq<;f=xJT6og;BX3j;yk&Oy>V6|Lvn=ZlYdiv+zu-r7QRt-JIB8C z=cm*gZ_8fn>g((*w2<-3emdDQ@}~Rt>J3aEK0Vcrh>dk!7_h}J#GP60uCw~4_U+FT zoOl>^-EE$5>M&@r)s6x|xqj)5HzXEaH2hIxr=zErl$x3v;Cb{;pV!hJucb})|2}Hf zee8};N>09PHBt87vfFPXZWor9&QJVt{LVE!kyMF}$}PFJHs1~#zAUp2TmNRa?W-TM zb#H#l{!e`Qm|I-WDKvC-`kuRa$2TS)XTP25*YV^t>zZ}zk|KouJouW}=ejaw-Grju zu>}@$3M}4$8gPdW*_illKDH!oz4GLf4uL5*-hMmyt^eKg4eT{WGY=g*rY6*BQDY}R z$M3md+Oc0ch1|>B~}?64)6ar2eh;C z+xN{k?%VH@S|us+GGgH>_r!D2Apt9GD%N=g<`h`XZ7#c=s_kRA=E_8mBMVnOYnq51ucdN7ZHw&>9|*AfpOMUyaN*mEj--u0Zk)H}QL^>Enk8zUcjw2g@ABsqMY&qt z>vHWp4;QWuTOI$h?r=lI&KSAOhhHtu9Dk!Y(?@aUpI_^brfu#_+uZ4s_P;ad`UJ^2 zD;I~YR-WloxAZsP^EdOpzW%&g!Y7)mEpg9>&@9M(rHhUyNwC+?Yqc&9`*<^b{=>cB z?}1K8a2DHXBiP2ctFKCdmJ+|HlHZi`&Efm++6k$fbvE37+o<7n z>AB?1pXCJ9O{n@&DadcfL%1 z>2+iO2>ZWZjF$vug3bdw-Y@U0G_m3M()|}*}AVk zp4GBkoLuyNo1D)B-tXyK?p{0`&wqHq#fOHL9LBx(z6Bo3|FTiO=FjfCa_eGu_kj;) z?g5Pw2uvc3*?AXDu z|31HKV8Gg0Pq(g**~zs0a%X2J=a0X29+OHkjDFlJym|lpqfbvy7wn8FusUmWZq_wN zyZ9r&epzk0`G)!U_RBB%)K*;=?Cw>Tn}6)tGqrUwJFOxfe*KW_>;eK`mRxAOGH&Q(*YJ zI{#s}{yrD0xmB-z-^$DOSQ@mW;Gxp1uWU^R8DjL*RXmT)wNBgrrYyR7+186MOFkSG zk5`&~_Tj6m*Ba6!nYvbYs@IJ>ccfZ0ArlhGl~`LV>$kl4!uqP`CknNudPPJ>AAj^nXiuCxLqdJM{Pz6&a$lnI zZ3{wU_vcT``2Xj#et?FE0AKsY{qv80`)0Q4>M7&jK9Jj5tj?_Qy5(v#(`EZ@R|}ao zjjrZzK3q(V4^I4?tI;(#8hgg$LVj)pRc6n0=Pba}wxq^}TzaEa*LR+T73O%aa+!i*_YQ2rzb>udbH69=`Kc z!T#I#{(j!QK8?fu&))_1j|A(zo9n&X|5g7uF4x<4zPI=2zh;F8G3&3-mt2>mqNJp> zXu0#W)YVR|d*j?UXNGxJhUJ(^A9~1yjAy-2{jARRJFZr95B~dS_vxq1{&XSR z^uzaFPxN5n=KlQSr2jt|Z|`n~hV1L>9v^Du{_*2+zx+-YGw|vB7tN=tcoyuo(^U2O8Sj(N zKcDZ7JU{2`vO`acl;!3-sR*^*e%rR_1_MXNc2kjV*A%1QCsHo0xc>Tlb#dnl*5?Hl z37a)nmhJxg-$bg{zj){0*$lfFs+#Ss8tl$INw5EK|FHeB*!tuT?|1)yux|hRe>a=| z)oy233)pWen&lEG;1bxRFhfy2?gi);?W(Gj$Tp2G0aw9II?jP2ua&-UR^U*2^%XR^ zG2#644<)NE&){)(U%eDGtu$?_`X%>S9sA^V$F`nb<}}Hrr@ueBriSTk8uRYmyBR>Y zT!Olc*I)Civ4gu79dR*D31Parpc{2(Pe_&Aq|>XTbE#DJZyo>LJpQw3!c7VvYBCNO z@Vt5desP43BWPy*_fi>YM}Z43w@ma}8mJ+%rqb3yB1He4!)22%zkhjK+PQp5(d=T1 z+_v!gWyP*bpYP2%{%tMC6^}_Qvwa0qA_W;3_r{6;wS&xa_@91g(0ezpUB&af>V=$b zb29BFKi_mSXTwd4l@4EQR<7a--RdZ!x?&Z`LA!VFK9N?;(6D!}t%($9~SMLcljGQoq>qGdqP56RZi~G(Vknj z`*QWY+n;yr6K$MTw?BW&?O7M!f9E;i^U?_1!u|5aW0FP0MP-NUUk`p?|L^lX9oWSN zN=iyqelmR4DUoJUy>9ETUW)tC&31p1gD?BrvgrKn)^6*sJ8!>TtEbBjn!AV->ssV6 z>4_u%Grn18pE;lA0}s-H+#)D=aeMgo+l^~jBCaY(lpQUoehnebe>bNsIO!ZQlJk_#rLp5(eB!*>~+tn?2%_^!X+tkHGh z>%Rp}ytf@eeuLrKP@XF?4?kq(8~Z39NZaf;yKK7 z9dv;F?!6PI1^nTIG&(?@adG)lz;i{WXonUrgU2Km*NKX!yF%x-i7)Kv=y);v%$j-X zOacj;IVY${?v3N$88)Sj>)?ZFCzUY*m;r%a6WZ3G1cFLEDQv!Q%P zswxBO-Dh%w%%&DH%MAZdOO@B`-5M+;)$m^Fb zcvdi5{V;>d$(Vz=n{P@iQ&dt~lrL%=qI@_{oKYfdLD~;%B)=$GeYnJy#n+z6;4taT zDU(&JoJ5X3-x3N6C;xuQIfl$f+8DRoM!V3h??aH9yPNjq86aP6(s7=o5*Glfo$}Wv zE}fNF?d>R#AaTq#F&W98MbmG3i?7&e?UO9Q#^Ts;HmzA_{}$G2P>`!Cbt$DpZo3u3 zw_n&0#XBigFPTisjiYU6YHzvu3RIBAi8U$ogse<#wL>*H0}wnNL6b2O)8zwsAywT|Nu>2Y$c1nfeASuj=kRD> z`S1wjg1vWGIGRA$(RC@U2A7*4K@et~F#kK7js9US)10p$7pORhsCFdr@A|iAduzu& z%{JYKd*bxx__61hNteXBufGmzYGzysTyMVb+vD4B%PRKD`S|%Adi>Ghdg+oN%{TAU z7iLQR`uk7#jx;=k7Oj7_l=})#cnrVx70AFrN}PqbD`(RI7hb`cZHcoUAMa1Bs**aG zz;HKD{A(3xP~XhVEMR?p&D-PrEKCuRk;nG`G`Bx|=@KZp{(JZSpWOdH=l^$wXdQI` zWpa=|1O;uw^<5V{dpC`>Ng={fiY*Hi5^asM^3$|^90fq9{%+aQBLDY=^NLl!4&46! zyS8L^?3O60uA@z*5AA24b-cVqeBIost5*H^a9BPeGn2Dyw``Nbf)K5Tm;J$8PDQwm zKHuAUpX2f4$DlcRfqs8w5iUlCvhs4!p;n-apd)l_pX=5XYIHR@C^T$NKmXvY`TpjQ zA3v;PBV@u1%_jTW8b#%@oCnd@3TKD+M%E_Q9;OVC? zn7q}tUv~K6fhSK`>OM5vyIRO>tJ}A0S6ltBW1kfL6n|IT2MP@kelhz+wEPOoETuw;6^S!I z3F^g*_il%~>h6nlElNmC_c$h`r?VeT;#?XuvvF2qfJO`GO1<^_ z>-au@E@o%|4O=rzIQ>*&#ZN97KK5zTr+>It{r;i;zemRdLPa;#6ugR9eO19zXkF~? zZxQDofBp&DTI+CtfB9ujh7PyI9Y>Qk+{}r8Z5VpBZRJYMKX20Qj~+VI^!0Q7-|P1u zY~OF&AGZ4IA(ku=*T%~)kKDPV<2TQyvH1D9#5A?Wj~^vs^u!q!+vD~Y4^1>>JOd%Hg$nD_kqmA&D%tiaKW=iB_2H>VhVz43l= z(axT&TT72TE_6{6lxSrpLz}xc~3%eZ{F>#^Ck|D95^dk%&75$p^hvfy|~~ ztC**HN&c<7ziL(T=9L8&Zwhwb1)-gHKK!f^a0T4}z5m}=`^7;kqrNLHVqUdMNrDG- z-_^zlo=GWe=HI?=-qBoheg*^dT!5;tQm38@ZMrFwcYj~=+_`s;SFJ0t0v!jy^0rL+ z@ngdmyZS)Ux_b5MjIB}rN#$`KhtEIWxihln$3u3D>Tf(kofa;EF3T@FF23lnGp0?W ztH3JuL&cf%7W2=ugD*|HcK!Or_pd#cUp`-54hnBDj*HMwUEn5g#vbIuFG<>B+&fBN z^WDnj{aYvBq;Tl@XC)D?#G)c0xqj)cMF-M?4U8u{SjaeOO+9vY_Vx=oW*6UopZ{G? zpmwo^3?B;xCGX``R=bZ)lF@4fAscu2VRye zxczovjNam~)wb{Q_FOoVuz>gU>C-<>`q%MHKg}8_k|@Ep>Z;bImnPS)U0bNZwdkVZ znRDk9BzPPqskAsL>gek~ytDIqz{(KG74RlX6_0-8f;R05pu$-$=SQvQ2CaQHlXG)& z9_9c4_C7#s>Vixu2F5O>!_Pk_78Ww*@BONFukv{=M-xNjG|)0Sn2-$5%|;?}b}3e59pRX=R7MxZxQ;zfy7|KAzej(ar=LQpd$;9Jia_Zg>%)a zq_Q%x{=M_C0Cf4Z z-gIttKbiK^Pn*uBHJ?p8{G)=$*qAdXH@BZ9t1UOI#!jB8k>O9BybK?xLSb=q*ni)B z_g%NGQLf;N`CNA2wKNJ*;9wGQZ9Ma8^!2} zJ1tb$6Q>`eCobZ;aaGZl+qqY=NRoTrwrZ7BT4AW2B7FqAV-@iHi{GzK_prw6&=@ZUB4_tfAz|^$U zLxpMoeRfBIwfD`Xcwg-5%P`@Zf1W)wbmG}(6VAR{9HDdQ@yDd1A||`}`5$gEah zYueXLso~H%=UdFDyqf*?ZR;5`JrAU9W@PyD zkKx#{qzyNxO#IEGUv&Qb`Nvno;}724yxh>zveV%}&eBU+TPK`fBh+5)8Fkpuf2ZG!TMkR^}QcI7FO7#H9UH5|6jH4=jr%| zpPrsx5wdH;?a)8F>%ZrJ{P+9);n!as1X$Lr&wl_qK~?x?4ddj=lR=jb9zQu*-N4L@ z?eK-bl_4Mt`tAR*JU>6*9dspbZOKzX;|mWQlqMc1-}jl#Z;nOV&Yh7zYV0(+m}1R0 z?4N)1)hjK%m>mtEsjK8<;a%(OFIjXg;+TKFedS8cnhyu(gDz;jm?5%B2Xs4OcDD5Y zU+ecDoT?qZVfXIuH*$N99b05k_s0S>i+bkVIVCaa=F2Y|)=96pnswvm&7*H`Z~q8d zf^zJb+NPUtZ2JW{SimQuKKqKPf4#od_>Rp#cOF-x48bp@cq9`>ppf@Gn+2bIP|dK!SDP3?f!kP|7WkjvFPfgAN&7*-+xfQ z{^$7@Uw45n<#P#SVKABPE5cQIXHO;ftYwkwYh$lZJU#Q^*2Iili5JpsPr!93hOUC$I zxcIUpKx=BC-t+*isqSIF`g(g4tE#xJW{EBh;%s#)eDb|+zx>4vk*-AxF0Qbj$j#>B z;_@Z;)7zC#(j`EHF;Q#9R$bNFb+`FK_o`bDO03p!Uu{x2khWQIV#njN=4ZLTe*T>H z;?U#5!+G1IMICDF^lR+&AAPi#Hf`F6pU>kTbnEYP*nZn}zx}Vhui5Q?1kbVlt~G7i zvXWLyLYx9YsFjz&1N$#znmFrdeMZ}Z@KU!71>Jp$gl@RJ0EQW^E&9z9&wf#nHh);>epfYeZ!{RfIa+lnZvo81OL1trySR zo^23z`N!4ub-h=whL-G(l{t4{M~q&}Va1y7yWc_`EJ8)Kx>|5P$s-#$(5hZP>G?#X*5#`DM;Pk%vVy zZ@>6j1uBu_|2zu6asU4RsI>=<9XnR=>x<@|Pp2Yf?CW?QKYsk-cWr@HY=Fp3De?Z} z%^yD+f>wcidaC{7&F1rOf| z|2%vD@ip&^ZEah@7Tvkyva=>Xb8CU2ZZENl1ubZm>QxexJ`BFWltYG(ea^Y&>(g~k7g)#??7n-g&T6jTYON~|pU;~= z|8VvDTK?sJbC3M_V{_=C!5lw!5!c4U4_gi=x@(9W5|6LxbX%Mlx~FnNhDq1XosrrL zj7?2h_upsVeml2yXN{fypE`S`i3Vxu!WN~kRMy4qZL|OLkRNoN>i5zmL76F=EkUb- zS7kLXx>4}vhGES9y2GnBm00{q-G1B9+`QdIiEsXS^A*#sxGuivurkC=YpT=!`=IjB zXeJ9o$!^<>S(WE1c3Ab@&2zW0>s`50^R@BX>#rAvXl-073+Xfp3SLZ3TJTJB$&+Wyo$IK!ncp{CdH(ssX`2-VI27dOyBSEdTz}oD zbNWIl^W8jlyZQXTf0vb5#b!jE-sK>|#kz6Rrk)Zj-opj~I@8>?oqYUJL2iEA;fD@- z)7_0`Li+SN{tGis6{kxad+_`E!E>CTzB$7T@i(r4E+$e(Z*ETiczOOmR-sOdKXvxM z{@R)NIxh`sOl0U%;%AbuQxW3S(bIDX%{`G~IAxtoeT96_=8??b>)V=R<|fhYFhx7bTDvjdr%z z*!9Qg@gFwW&p-RI+`*Z~=^qZu{}V_*H)j)*<>7CxyVg02xLV9jTaZ>(#s*pmGiQ#2 z-TdRd(zYVqM}OxnKmPdG86T$Z?(PnkLlaa^ay6dV#Iyc7_xkIg#Yj8u=BZ3x=~%ta zonMJ3T1{8y%;UrS_6Jt4pLg`k86JiOx8FKlHraApip8;I)h&TuHv>Dl&By!$MM`!* z`!}m`@#6a(e#?bFR>*w*Ts&j$T-i6>9e&H1ci-ikewuaq>DG;g#z!{opYOW*YE{O9 zV+K4WyJN4sE`9SJv^;ddDlHkl;~oO5@8&HITIq0|_eqiEm8{YdEBA2OpipTwb@hke ze{bBf1+;1^Zhe2%UOwN;7v6uD+vzn~qYJcDwDgrq-LK304;(qta`n`T>#rRz-+J-A z`r0E#M}Y~apT3jkVM>tw@1oTA{eC@v{U`PJ$z^3?i!_`rn`F#ti(1RZ(0sj<{b2z| zcX#)U*|VK>%-T|nIDNAh-^~NHIcSx~-}w$YW^B_>w}!1Y4LW)1;fCbQ zOhI8`;jn@gSF@7x^Z8eXoLUtXq9L+j@8073Z~0l6JX9VTiEBmDA zr@Itl*vm?a+0@^2^-{&DUWcE5KKT6e!KX#MY7@?GdfK(6 z>e#r^@W|uKpv0gz{qU<-QnSvgrQF=pVQguccsD;kpZ{mg!=3N61qB6bttHzGgH#{6 zDswc68d%)f=XTk|M9TNYE=ip*5!Uy6K8yW(;=d16!&%Jr`|`UsWOXR0IrHtU^@UQ| zhXoSqelqSFB2AZHI+#cu+*$nmLSFgD@Ad!rcN9PG`zo_Yoz^8R&j$JL=%^bO{r~wh=K3CZSl}SQk}<2zX<>tPdDy~CsW)Yykq3cZw*?wpO$QbFWTOxA z*Z)vd@jUeN^ZwoM%Vvcp{h5#|c{WY>qI=5Tcb> zUHyCFai?jiYu4yIUc3Ds=ork-1cQXX>mqcLva`8cojA>AH@$qR+0)y5@cnmZ5my~O zy+d!mEr`%r7_~O);E%;FN{Kb^tKT~bG(@fKI(t_4%Wtbaar|Q9;sRW)3LH!zq+~_RJ4{^gH|D|4U=E_;)_Dpq6b^A z#~r+#zrVG!vvcACpQS;LMl+oj1~llLo-u2di;djzM~@bX?y~ICIsM^h4QNSo-KWX& z$^svvuf_(gG`V*D`o`_s*&PK0dDuWJvJDLl`8+2Hbh>QVw5jQO9B9=y`}VxMpq+PS zW@Z;NOctzqwct~q?8OY0;^NIW?%i|SSrgEBBCk9lGgEN#WZ`S)y_On*7CQQ*{a=`D zbm!mp`*qy*KTpn2tgMu5bYNJN!M290RcYUkN8W4JuC>1;0UDMDm*p2PO01~#x|O^6 zme1y!E%)C$D+o9&zPN$Obk!;)Ax`Je(2QAaMLT=aHnVz8GB6W!HIM*pvNoH2_})D^ zleN5_lO7aVt}P4r_`@V?s}xu3BjxoUKts8|%%r!?|5#-UIy4>B6zp|#zqNmgm+F$s znI%?t9Umxn2&?x!zOhks$KAXayZUm>y8G>ZNtox|Iq>b9S;<{cB+j)i|8UMa|M8cX z!2u%13LlS2KL;(ed-L|~qN|tIa0zbDzwc-u0b0|#FuA^7zWm*qMIlA8da=8Dy2bSqv$MHpU29vl>c*Wr2UZ5BS*sd{2p_t+Ieo*%jg9yJ|9d>q zYyFVYzrRX`e!-`cghTcUJ*e0?7s>3r;=@+fSZ(*qBxs53>-F;wpPZ~N_j2Re3wL&X-LW@L ze7auj!L8ZX12sh09;(RmIJd8xfAGqcDWSe4b#-#DUrX<({QT^}9^LN$^Y>c+&wIS* zbIt#CPNHQE3XVBe|_$gCag|y|V6?_=33n2SqyrLR$kg z&Rn>_aR1k}-FtpMvs`phqhSBvV719l@9hocS?s7FuwecA{*4?B{23(0vr|93mEntrp^8<7D}><8EF^ zsOn6g2|j88SIf8+=bt}*<%)`XU(dy>qVIYl^`=|a|8uVUwz+;mh}Oldtpz({&YZcU zF#B#=P7crh`|YWbM;;qiRaIRu=}p`G^G4;Yw8=4g$IsXQ<32b4|AZR5{@mL~k_NV(^=dsmUH^0b z|8I8UdNnL>wo3)Cy#BhRwDjR;-vzIK#VMrETDw-)?(dgJ3ma#Jt#;iS_3$vi{gZV2 z-|;3=r8=jNy)B!3a)}{N^TLIWF?!;8+h?a3d74Pw&DJdavC*Q>d2QI@u+@3=PnJe>tbLfn3H@EpNPAvHQi>LnY_4>o7PH7!|^!Tv3 z|D&RvAAbMs>FqVLvAMERDR^zz&Z@6kpwWaUf4|$mShN20(?vlm->qFQAA6nO_F35K zlXI;<&q$jbu|^zxgiFk>l9@Me?)2L#KmWYD#T{3RJFZ$&Z$-z?{r6AgV#aQ%iK&rK ziY(L8@&r0vtQ=?lT@_}YfA7_u?f2g=-fr$yUvGc+?%UhjquyRiHP63Sl{I@=gwDB{ z#_2yk+y6Iqb9XP;eK%s7>&_U(*=Ii;WPkqid;R}%Gc&VH6RG%$v#m%e{k!LaXO_40 z=g&L;ywl~@+WB%d$!2o?0V_jJq->gzCLDYH!*Avao44k!+^vU{^P8;toR7| ziWCzUx4!B>>7*BtkQz%QF)_=^BB|KxfuZ8H zwY5uvG|S4$Hsn^XcRDNk>8FgiUd)24n@&9EUb8m0gvlyCb!${_^!B{XyLSEZ2uLn2 z?rv>$?UR*Om*Fd)_j_Bk)z?JKq4h!fo#u>{K&2GAD@0czkb7(lKoH5)h);_ESz}W?)RL= z#=5413VPGK|NXOLWBYZn^zQ|;?59ODZ{D=rw#`VQtuc3d|Itl4@9suFytj8Mv*Pc+ zuPlGBP3Px+Wy9a=*4)}!`Rv)Vh{(vlYvWh9IKBPK+v=1t>ssXOqMc`sc2A!%KmJ9` z^+Fk))1cryJ>7os`t|(@1|Mz|>YiQ{l*#_QtgP%~h0Tgpo5XugzTcYjNsx!_<=b8( z6B7@UTbDV%yxFdEdCTv{xi$L^Ow|sbIm6=E$H&18E{orOONotrc<$bd*RQPurk>oS zbM3nMj_U7ntxg|TK7Vk zw6j2Fw%g*yRM{Hg`-L(-myejtuD*Zh%7xOm7tHU0Q*Yh;r9qlOE3XKz-yCn4mYUkC z9lp-cyu96MVMFfi7AMDFzklqBN1?`MJFZFD`bMm&{K|`Qh-vF*J11MY%;6 z%H+FaG6n zKYZBP$KPL&tJUgi>$k%uzjb%c6Jrqu*Q^l2Mw;`AP5RNKiF0M;g@uJ#njIHiEQ|c* zv4ng2Y1f@GiRtOvgU`i*YGS9FojRwJa&vq8?f*d;fp9v7v3<>x@~~I0H1goD>8U1UTw)ozC?oCrhxcy|y^iAZqQkxC>9~ zb|mljdTi%gEXdc+$1;=Wa6+|pb;g1vAzFsk*6oii*z4vkxtSBVHcXyVV#{g{Q@Lo~ zC9*LU+djYeXtS&S)mN*fK{KP)e!HQi__U~V?p)bfXMY#pT3@kAepiLe?YwZ0s+fpH zvOCNh+pjOK)zPntj+*AXH0bZ#Xuh3y^O_C@Jm1Ln_Lce5qSmafzixc~zT<1{r5|_i zzWs1#Ww83p84~YV_L}&*hlYlP-sEoCIZwi8)pgLBM;kY8JbLaNU#AOG|MAV@t6r+l z^a)xU_VRtbNxn&mC zG)T~c1GPinjmh+UB=}?m zZo>KJD?_v@_TD?b?QG`OsF>YmhnIR!Kbf+Le``!ucx}ap%*m&p9(r2z@MFciOunv5 zd^cnb@j@y)pIZ1?^^VrZ1>BTFBJtif>vK$arKo%+s#Vm>n6T; z^7tDa7}j6E{d3u>WiMHn82{A$UzmCXTsZE_Dl6MIbLP&I-@cjI&F_DFT-|eNP)T|D z;@5Avgtz5x-n`k{Z1>*lL7)E>E8gCgX%@}CYUP#JrCL+9TAdzl6pfI(mb2}~jT=is zwDv#Y-y+W|u_d}?RXdw$Na)e0MSDLeT&S}+w(b4=?!Q{P?XCMuV~@Z8o_yPETI$6d zvyD2hm#*6ht`?bJzbo2#p_F&}>DE+LGCdoz!hlYOF za@lFs|DJv8_3NKc{IWao_S=Q~_s`$X-+#3J-}(BU>$myNOSU=M#Vbrdy>a($Z@uY@ zBXlla6-}$C=(u<9MeyzPwQJWF{{E(;?l)(_>ebo$0hf=zc%hM##eryr4I`q`jPs!1L_Gt87=1aAi zaXcg3&1R3&!gblzqLy(vtj?!5X-q%Ou4P!YS5BzYqHh2HjoV+o>oc&el{svnA$B{r zR!;7*t1(Zr)53}ed)|l7`ttkli(P$*GCAHNT&!e^7VH;#W{K!<}u$L+UuOV);1$Sm7#UX@TQFUG@WS^iEZI9T{cOw*Auj|B~e17>y&i6Tz;;ywzm1^O-rq*Tz!uhtkOD}GU?;T z#X0%uFJHcV$#P@UO`hxPVwLBgw>Oozwk|FB(fj}Z-v7M1-tI-rwG5LJPmBELPDg6# zKI)$AaF+W**47hGi&Q3aGBr9ZpHr(hz4{4n!s$&XQjDtai(TVj`P0+acPVRYKqzbM zb;j1Yb7jNA!ZgLY=U(vL8IzWqE4%%6@BR1bIXP3@7CUaf=@Kd`b9`Zpp8Y4?NKmTj zSidT3>#1wk#BRPZyP75H8n~n8XK__^btS0P7`3jax7X0Nc8+>03|a$NEipwdFFU$E1~>b$J29Dk=v)4zXp zhaL*d`j!+gHe=bzW5?Kb@7~?h!^8Y`!O1IEM2wBiKUAd~`U>CP7AVrXZk>U!u<)N3 z?*G{tu4eTnZ9H*x_4OZT^X)1#uUf9UD)q8tR?y0p%vmpX^__mIB*JBASlClzcmD3~ z?^f5tMOqgwbnKI#f9QEOdwb69v~u&~-@ci(C3-BK$bKa&)MzHp)+pZF+pI;WIxQ5q zd^uR8n^jCqtnu&h=0hJo7+k)5xt(n@!<{_y+S`SZ5zcfEPw^;aVc z3y=DG`3rf`8#Z2CmlnJ-goCX)A`E<2c*lM7zQ+dU=G(tF{x+HDk+R;&rRn3x!ez_* zlV`c92TVTyJTf9;Nr+Zrq?q3H?h2cO7Z*SN^7U(J)IN*8%b7ck3=4Z+zI^;**X*L5 zE>?5X>gpOxtnOy|FV2*@m@(z@a{t12cceqKM8(9!G^d}Qa(bz|la>GFbvBPno-NdP zd!X<5WxoGgqI74Sy{0VcS7Rr?F`{SVM#Fb^qZ2EC>P<@(>?{Gz4IevxoU_$Qf6vq4 z#TOGYGiP>ocCtGe$Ih{-Y&t#NJ~{0fTdPxJ+UCw>J0)afp1iqf{O8B<`U{!AujH8h ze7*l)v|jwbH%qsAt_so0$<5ukd9(ABx+U5gufKK;6meQ8kiW08&uQU_W5?2# z3-zpf?c?iP^zF@S7N&#F$4hsHG#ym1o4>s{a%T)EOy0hoyH{Uj%W-ZtW*(j|6MfX? z-+IS+D>Rv_RjIqXTV=B6qKg_6J;eKrTT&yxeEasuf^S>ynzjW^0STS+&nX_jfe*XF5_32CsGkrcRmjBz3v=KCvv@JJyZP?8lH$ZW* zaG~O_UqLT+oqLdA|Nrg&zn0tc?xxk%$sIQM_;Zhn5U1Zc?XHUxw-D=RJLK(J*SQSaR~>j9JH~YKKQeMb$lfTqx6CwbyTL z*z(n@xf=pBPDJO=1zr8te|$%6^)#=ghhDvsYIR~fy(u76)HU!!h0UK2?f0Lbn5e9y zum9M>Z+VlK(aa?wTBWyy_iAj7ntSu6WuL6{@`#$*4hjv+UjC@D1Ko={$xAgdA_6q< z^|i{PFlpAwlb(0-%rDff+p}TAiBqStmQOl+`a<2mTqEP+$u3F<&G&sY%iBIXK;y*h z{CUa}JO)NaP8%a!E|&zYR9U~bY}Ubpjy7`s&wu_tbA~tX&W_3jyC&@TZS9iBjLn-h9vrO~u?Kyklg2Cm>#{)%L6ATXAxS?S; zpZ~JS$rPhC>+?74o?X4nz^A<2{CHn4xS}?{Z_|GJ?Xn=v&&Qv zzuiGqJh;el>d>Fek~PoR_cCbscduNHo&4Ic$vI`S=gxhrzVGAxoNXCqr>A&5P2acv zuhD~6r-w85J=FlEz>mLIgstAVb?UQIQ&*RiyL&91G?zGJudcp6bM|gxdAayy z6HO7WnB8X1{{H55`{xH}xP*s`r=OpvDA#XnV^g9KU~FRIp(A#OOl`_uaj{zgzo`7v_|em*2d5S9M~?;#FF9^Fb%i zH62vg7@@y=_ihm`*0S>Q$oTm5+S=w6qsHTp9WR^o$=P}-O-#ti5y=R@@Ug1aWVWx- zOr4b>uUgr%W}Q`gSu$(qPV1U~KcD;f`cCvvxlr2s@#EqnNrr9h?HMLg6*h9mj=6QZ zEzX$bwl^+4Tl?(t`Eea;kRFyY@nI~zIkj};IV{+2m@(7*?Do3D+W`cy9=uGWIxrISvo z*v;<`(YnfcMMbDkiZ}VaH|IU)=$VpjpqpMEKX#uv!=i1GM$6&EWy|{?TbQj3nH09# zRcq>_6B7duJzVFvdRzL+XIC<|Fvn+SXG^qouG=0j{Eq!l*HNYJ?$c}5t_@s&eZEmt z^ls4t>kt!5OU5nQ3)Lojf|f%qdR?{LX&Q3VvF%)j=F?AKZMl8&`R6ld&wgD0|ChIzm{|OcIsBKi zw$7M4*Euv+WwPhJn$I%s{oAf@TE4v9L*>#+k(H~oR;?<#Rj#Y2_woGyKl(BI?VQbi zKjE4dAAUA%@_D=6O~)TUd~!0Ajh)?k>a1_y&osaOeUN>BT5auI`yb8n55HE)Tc>S} zdiv_>Yta2!=dAA^cv|%EYn6wZ@Rx6HKd)`S_jb#~O}VpY&Xj)hCg=07*X!+rZ!dRS zEPEp|{{Dg(Jz=ia4;6Rj-oKt@ns;~C(O0i-8LxN#{oeh0<9_*B%N~CI`Jt-z#je>^ zdq19ej4}k~$7Fi`^!-t)DczGATwpx~1^!%Lcr=K=w(}JN_-k$f|d{bri*-aZZ z95{1^7c`yWqNLGv=*bhqPd{zEy}d)UM0HLt3ef_s$N%?8|KFsSFEc?!E_nE#qi(;v zZ*k&n(Df@ZdhS+p(-IRqXU>#d7q{0HmYTNq>X?0h zBwv>N;+FMZef80|x3??y%Gt=7D=Qy9^7teBAttWIhWQQ${5M8CIybjc#Py&7&$4A| zl5%oN0-(#Y+7dVH+Vi-pTvh_;<4UT))j*ww&peHs4W}+xGUYEazpJLxnO~ zTeY_5pZ_$y{^#{Q-)?0tTei$&Y0%s()2~%?ebi1LI;147AGahf|HIq-`rj7}eK()H zc1=va{*QEw-qe#PJ=xgVU%Y$wj%A+LQlr(YSL^8MO*)w(De>a@m6gHgLr$J7oOAB} zf}J}p)zsBLR8%#7d%QCw%iho2eEYmg*{-8P=6QFLa&o5p`)Aj5P+|9O%XfEn9)9*r zt+%Zu6I60T+72y0Hi1Ul1E+j5pZxtcn?#$V-t^?u)YhPtD)}?O?Zg_!X{n`$lo}iM z7ymqVOl?`F-KRUJHtC#t%GKT79kb(u;L)UsE=qh`R=fZBYgZEM9vT|8R%}`3rd_*E zJ$nX<%QfrP6}`D(_~@fWY;5d~yH?Xu1J{P#ymKe1va<8M{r*$0UP z8uV_9{HEO5YuD;Nd;Xl0rD?^gL)XG)IVtvM&gwt2{qG+0^N*6OH>BKtTX5)F*sqD7 zL)Kre?Qj?U%&19`E%#aDJPrG_T9F9vyT4# zySJS;?Azz5Hn}pVd%@q@4<8ox9g}7`2OEgndP?gdwYA2?`zwb zJNxckneXq`KKQ)#w$9VftlPG2TM?q=zh}MI(o?^Fu^mj9;H1cZ;1%druX$mszaFUC z5)pqtp}M-=N6p)8_t_sm0;{TSWk>5w+fn^pZ@PZ{u}6=D;?}p{ylMIBYgBsA)~#R9 z%>ZA`bl~f2<2Uc#efoAgbJ;RA&!v-g#7Lb@n|$|fY~H;+N3X8F&d{2)@x;Z&?jNgc zuVj^mg@w((k#@aM=G0RoY3bQK{QTv5>%&&37)6GKh26Y+w{}@EQ-I#|nX_jnS5!=J zP;gMWboSUWw(IL+lWS`qPvxABRHl9W{p2O@gZXuL+x5fccKKAE+3)`=Q2z7!?DzX) z`TzX?*KaP@->)iEThDh&dTm(tlAAeY_ukn4aI4t8W!LGahu)Sw{`@m=ivA{+^L~LM zBCZGDe}DY7$~yHe@8JWRbRd@wJerymDb|)~^MSGCx8_bC2Tb)!UbB0{Kx^qv2&Zfa;!vX*BaPhkR^0E2bV+ujD3XW=% zmtM?}so1yU|DUt(zd!z5d9l=d)-$I-5gGQJ+lNeirFz+>dWjx3;H&wt|NPUUho66P zv^upo9o%c+I=9BIJ=4rOaccTf4K8p4wd4HrhFSF7-~230EKF~c z3wN4`wC>f?Ir*f`H=CU?dNy+Aji4dF_T!I_J>9kc-P$nj!v-s^ zW?jiLRb# z%|E_r&HYu2mK=Y4@cHM5-+zCsxRWt!-*vSUFZZZrnDDH+%GH)=_pNa8Ypc6=1(_NX zmJ3bHoE4&#>b3Nf@XnnZ7<(<;WdBk^^d=6zkEAZu=~5b@!NtE z*REYte!OCpmx_?02-m~!zZqChXO&+2>$z}Nf`NmI(81@s_y1pYRqJWd%%Yt&4>oT7 zzgyKMLi3P2c;!?nit3zuF(0)c<}z zJnsh^XsRoCfg7mF|7i7-Q2r|%ibqzTdHhj;tM$dpmx?lc3p1ti_k3it|MT#>q)bkD zOp#1`{GUhje;(EU*V-+vZ@ckOFH-w3-gSR|qg3Wkct7+H-=8}B!-o&c*xMa_@q$CH zzrC|F*~ias!Ky6vEf)6sPi)P;Zap<0sp=6}uLsFZ;4aAi=6CP%&dswuES*2+;faaL zJ7V6Nm9AQ~NP`PB`@OfS7j*lkj@bT!+vd=!3*?$3-Rg}}F#e-*0gfM%l9Fi^6*CSV zWMqh1E4DPKGe+JrjFbGAn9rTkdWB>Q!LRLWquZl}4$LL7L!s>6*BbyS?kO z8=4+l+}PHdI76b%ar4b7=JUaA?G}OcOLEF)=X|@aUo8dE42npn4*iKNSx3B=zP6g^ z(em-*;w3?vi!SEH=!vfl>-JiD?A9%*?Ck8E+@*#*k5m1_!CFv>cxIs$m8>pGv2$#z zr)6wmKVCRv?%d?s+UBH<9gi*atEFJD|ZTbov_>dK8a zkz%v9w$91PDSCbm)C^p+X3f4gE9+Bv`S=X%?EJ#RB@P=z#Kxu<7f+7S`@U)Uw?i*> zotvG%@8E$0469bj{g|65F~uu2@6HZKwaK-f#x<44jvuf5`}OjQRb7iT=APC)y(nz; z#w}aUym@oSd(J{obc16IM(nWwyM6KAKiB{NsxSKUqfq8JXh-AvA79s>57wK$QRj84 z|9jAy2p(Qu&~2>^-|zhv%iG@f*n(eXebXwf7rXq{hJgl=Km)#J=G!;uc;B+GbzHRy z)L!}b=KMeXw)S>S5w4mK2iO1naJYY8Sff<`@x%S~f0Flop8Nem>0V)(K#|s(eFsjR z;%aqzm@T=Y^6l2(KmGqd{(n%myKrZWLv<|AVTHNpK7f`DqU2&oB-Z45EO6UX(6z|G z;KKR(U;h6mzI^#OBCK0GJkQ8}|A9}RVx!io`|po$%T-fX-&y+l+v$1txSv|^wOw2gw6dg0dDgOp zuYaAMcgy>Fj@inv)h4rjO?(eOEjqaiHBC5iDDK;)5}+KX;Tp)n!t~|q*N?wmFaP+P zIc0OCUhJ+jTeGk4to|-{HchzyxUb43-_=(SJuNDHbi^b%d9jbbe0q9%IFspq)#HU$ z_8)%V^n2r}v-;@U+naA}>pi{n;@!KhI%3Hg881@(-!8p<75v0d>H?4Z!lPPb|gHQ|!-^3A(UVZS3siJU6$xdHx@jrrU2TZ(gomxh{71xwp4BGaPwb=r_mWocy1I z{T8*qu6WOziJ2lRgjNXZU#V-Im0-}ZYSk34)J->awnp)8k5-rIyL9<-bLK42;G5p` z^la_k%vmL+tuJ47c}UN?-;{g%%=z=5?f*aj?`|P;%z)?G_3Jlp-Kx47T)j}{_`-_@ z49pExd)qQ+Rf|^dyEp5s+SxSWhN!h(i!Z)lxdRCyB*#2@?Z#xP-*Eb=k^qMQ__A71 zcD+3#ZF14hmUY=0U7vg&@3@=iv``^#J^%LT_jk^1(&0J0ATB>(Z5V$GOGv0IxZCZb zBMx1HVh zvc`1pDW_AVc$sf+<6jeKA#>WyKtw=h0@;MUS2aZGXvkrMLV6=hSk5?7c9aWd!3Wz4-1pyop;Tz zZ)a`2zCCJf;OeV+&5{U!`Wxj-LBUXxF;QUaFPv_kRCzkbj@M+3clJYZqy7-MxGF# zZt&h2la`&`{qrX)DC0bO6r?_3vBukmV|4;23g?_-|DBw?7<4(EOMSik>(|mtE=x|E zKArzgSm;#uL+-oQP4-Z+s4SWV8jF|f7v^fcVAAWg^w76&eC6-%B&I!M1FtxHf4Hh@ zm!!1xA~vtP46o9v@z zQTT`j)PCwbT4g#qB-y>C;{Hch<4jzeh3n7Jqy5T1ANS(8F)P!d8cF z+g=V@DYfQ(;=zd%Cnnyjd@k!Z$D(i5sxxQL7Ct<5e9f9QMwXTbuUt_%-q(Bi?%lK3 z!e)KH|6jDb+dF0RP4#o{Q(wJ$l~hqN!~A|t^TmsTNgE|*ExVYtRb%>T9bMhR-``}y z*T;e8Bg;%%Oz!G94?#i999r}eYEh_Rad8l z7ZxVJ3Eweup53|I`TNfvIb!kmZ*9xr*cGdqa;>*S`9Au{ax!I-g92!5=-r)}%dciF z1g$-2udq3GSWQI?qxMMF+IKA zy8M~7;v5GB(A_=n_k89B4d_TpE?d2tJ2pRmdD>>r`1r0ABToYf6#)(-`}%gTrCb}t zS5U%>(}49U%!HCKF@w%@aYL>pPa3eh0L+X z9~Wu7-O0lf(s4Yw<#6JjIQ{N!?QvLX|2DD)0&6_vM3t3zPU%Yy?X#f8Hz-1Q9)2dyCb_4~JVXp4z&dS)i)GHve}u(bf|um8UC)pzNn zYR4Iw$By^ETf3P3IOFLoLjwZ`B|*>ysNa9@EKZ+y_wL=}eN#P-NXp8(hK6#rGO7E` zNyy0PxV+ro$jZtqKAvm-`To;O1$o%wZ+?q=+hyD6SCN(UNaAC~oC1rI1zy4V`T83p zc=q2vzczY%L{yZW!MWAj>Y>e1q;hEsXaLE3Y0wo5JhaL<5e)m5#vVchf2yW7Yu-@ku;hKW~rxcJSSwydq+ zj%@u@VN+7tda-oxjtm-1_NH zpFVY7z41geT6<0Scm{hyOizD*@vkqM z$BrF)@viU9){llOLsm(&{XDU=t?a?lu+j@WArAUf6l*ULsd!at+&Vg=G%FOhl?jCC;zCq zcg}F@`T0Aq&w6w9@#mi}-oJmm_xrx1-}il&4+w2d+k7)KaO?ehuTM-g{Pp|yi?_WC zHMq*(-?I*w`ZDxBW_9$jRJC!|^PGZ$hR4U3fB5-likGUH`Su?l4s+}1>nG1rQ&v{? z@%1hI_U1Kcp@u|%k01w&W$`o9J~`W_MH(raBlGUhdwQs~e?`cuj9JGXJYdM*Q#j51 zzRmFm4;bA0DG9nn>GY-y6RGxd`TJQX873afFQ;t2-EaH;w~ZXXvoq-U+*!VpwZqrF*?Pn)c=c7y>8Eq@@`5x(3Q=ebswK!3Li&aum60~#s$%~81GL}Zi zZrsq=yLYch_tA4d>#lxZkJLnklpi&*pt7UeZ7~lI&x4OUZr-``=-S%onqMznpFMjA zS{9SC$)fM@moHZqGEORE?NVM6TJA7Ty!$a;Nt4&K{m8W_=gv=E>C^*>2 zDNpnW(3`$8L~HKGoZAb7R&Ln3_2^vd&prM9!Ftm>+^RLEdgcfu} zAAbMc5%YGM%c@mI78WNiToBl`ZsN(588c@-eB57u_}DSFZMmCw?P|-7o?}rsr(j2R zzFwi3nb|z46_wKj&Nfdv3EGVC@bk|TDVwTa|JE!nE;sO z+WF;(L(~2D%fnV1+S{M+l{Vj5`582jkhF12xb2gdR~=*YdJ8Ok76v5bz2h@BHrCMt zjaFouNcrd74qh9!GHi8DPR^t6zb)%(x+-k`oq0XmOVzTfglGT#t4Gk-PO_4GqScm{`Ix-nl)=C zolP@5dp!Jh<_6Ul{INvaMb0aGUj4=#gxI$Zw6W#q-@0A94(*81Gn&aWzrJp2PTB0V&724B?*3k| z+xE{3cYB*Hv)b2XA4%HVayWKNl-TFbpYQA}Zm-(gm>M}(;Pk0eS~Gp7xhOSWf2}&x zXT#R5l?nmOeA{x%->tq|cj8dtt*hCy&Z=$8Eob8GTC`vl*9E>^D^~6Lo>*hvwP*vR zQTXKZ&U1!q*Q}Z3p>iT^J7@%UnQ-^f7q7TB@16TM=(@QK-{srV&euyXy)>DwU+p{eD|cl*{$s+xz#PALh3|^t32&{q+^srRSDKEz8`rZQHqL z&%~Ypz*XkLT_2@JS8|4oia`9_y9X(bc_}VNxK| z9<=hv+uNH#aeH38!>yWwg-K;H=eF&eMY`V>#(l-oXp9dRT2Xl}Mo)a!vV|I4%QA!X zre|)Q1=^{yc5jzxd-&4;5z*M|7fr->tt8HQW5R~WBv1mB_xQM4;TViph)UI`lHMn{~W35ZC?y@gGy~)6La@1P4)mNQD zMepS8y=?aC^pZ@eyzO-fwX@HrsZU9~ZT7RKvET03j06LY1J9nREe+aPEPHy>iV!W( zXz!)1;YU^7G(--)Eqg!L-lFIUN8SGQo0oMhve~-GZ+GcMw%xleS6zLTA6j|T1hmNh zWl3Ao#v7c0dpY>my?^n7!~W+<`H$~*zn5EKBEr=wDKD=sz)_O7__ft<#WlxQUVpu2 z?b^vFQ#7Y~MNIoTmE+Qz>JJ}NBh99ItrcF@SeCHj?~S|*873QY*?o&2eyj+%`n&&R zcY}!>T6P1M;Zxi}ffhx!=S5ziX z7WU^+xUuc-&ineo)p}XWS*N6K=l^{m_uJlAxH7ruV++2v#DrOA)!9r|UDaBanGmVJY|_c_nb%)GJu%U6-`}_2 zfBgHt-(5lA!0WFMigp_CG*|8Idu&m$SME>Ue{n9&x9rR6#DBlt5m&}KJ#BMmqC~Z5 zk(|BwVS@~lo^+0r7VD1~{p0g4t*bf;Kt3HshOEGgI0c7D%bzGA@%0m%3U$vZ`<3x zI5^E>Tdr;K>_zR&VsY!mr=M;;{q$CJd$S@BXnR|V8Js*)D5IvXZdv}0*GBHUc%&g; z_VTYsk3A@tdA$AegI{^MxrM*K>FnNZnYHz|?)-1s8e3Pd+44QzZ1>+IpWj;ie*SB_ z^@_h`uC=wAk3L$+^|uGDbTON~G(;;|;+S#o=LfUuCeHD!*56hj^W4FETl?1ATV85( z-KvzS-}>!+{&qV^jf^zX@G(>!v;t(4&aAWF*x!SvmRgp*oO?feR@?R02Onp)`}1*M zF`2ElHmo~s^U+>ubI`K>y!-RsZea)t6S|4kP2cY@3@4qjatPKu)o*C*bbNposZ;YO3|8ZxNTg}Ho3ygJ6 zA2R4+cI*vXeU+2v&HDFs2h!{HKn?1D->P|dco_Kh|GK(hzyCku_Wb*4)z#_x zK8qH-mO7ZQL0D$x)m0`^$KJ=xeC`>tb+3J&{QO5}W?t`fQChQRO~KPspxt2m9Tprr zSePU8ylzLX{hH5Pre*!sJsYDpHNl|a`s=rM^7ikYeEjjz*IyT{;#zy{-@m>8CF(=J z=kM0K^zU(Dee9Lx>$)-8LVxy6a5!5Ww)UEaSa-`|#YrbwKx?Gt`b|FlG-GR2PhVfb zuP>TT3k_DSvMcNPKY#D{s!htf-`m)~Jo|N7UM2r^wwwt$le{}8C7tQJXZEc0|CY|$ z8|Fv%`DmD>m8GYL87?{^nPnQX`63^0IO~-C_hx_D$zPezbXB8-IroOZ?;0g7t`)mx zR(_f@`FYyi`{w)O@AGGDwc=@a4huKFv#WG+*y^JfFB(4nDA54fKWSJ0QM|q{QDV-# zd7oa#|F=DU{P^@cF8eeCMJ~Sm_TuZWDPF2&Wo4iz^S{bI?i;z3E2{cw(;C zuBFKkBR(7mf<)bG2>ML8{ zXP-Bpmz}$Bz%%n~8jEAYDy=7MytJ(_(_7Gw(Z!p zYijmCPxedpt7k3=UK#Qv-M)H${NGpposT7aFaJ23|L^zqI=euT!lI%>A3qw-o-M7T zum2h3jr;F|*P91Tyzut+`$?yt7RI)_E%sb|F~LM?uYlyzRa(396g-XY=1o6uzyJKR zXKX)f8hzBdb8oXa9(egua(>+}P5HWt>2>?(Z(AOhUrV@|CXEgJXwP6_3P|6 zZ{IHCQ_HtnmKmfYwjylxlh2h5pp|wjL!7Rg=G;z7PHvX}`@*c%sZpbA#r4-Rva<2F z&L4H1d%T}NA}(%Wj9&5YZ@a(zuFcx2DwFPJ7oU)x-fsVUb9~_1u$ex~?(F@2vAnKs zTI$A)8;?DEcI?hh)>BV~e%ACkE&OoGPwVl=mb~r9bIhD4seJm$WdG}8e}G8q%aUUS z7EA8Gznrs8Z|A)pH|2Nt_xo>+dU$Iq$gzeJJRfc3ua}-k15HY7*uK4c)vC|6CYffl zE&3+Ed+s}T|GeiHUzSMp9%FIr$ei`&?b}b!>;I+uEWcdxHq$JchpqX#v<+r5fTWEd z=?wx`YL3sDH?QFRz2~5;{Pbz6k>u>&-qM)sN59vm8K=t$Dlh-&8=qn%`T6(#`>DCP zQ|JGAay(>pC}?FMXfc?~am6mDO*eIxWiDE!^|oyG=g-{pY$^{W82kxSp5LVK;NITz zE5lar`Tx)V zIriAF*R6T+;$*4bW93pmmuM7O-Jk#0=b!7tON5;k~PCxw# zbP^`$0JQ%-k1cE}K1{g(|DWFfulxVKRdw{;efQz^{eS=8xOeZ_vEC!SFXR86u0OcG z?yEYB;{>;$%vpVp&sbPkJoxZoLfP(G&ljK~;$_LHlP5Lbmd(DI<7PJdAt>XP->Z~9 zUbth&iqALGzyJ8U{{Osw`+q;C{*J~`W^nx5slZ}--%wn<)b7`d~AW z#w<4xSDtofyI(JafBj_y75i?^N%K{rUeyZ5)_V}+AM+x=Aw)Vh>zz2z%^U)Pd*d(YRfNAm);L^tM2drm5d z1r4YF{Anp)^Pw7~qi(;y%yAdc>7`5WzI*VSaqBIi{Jo{!?ELp0y?D{_`}_OLdD~B> zvP8dAis9E^#_#X#AK3pp{(tuE?e{_XON6U+i|)MV7hjiJT3bJV z+;9JI{=b*ZdU~&>yYMc)*irwZyO{VdwrzvOp4LNM~lu)R`>sL_r2YNA3r3{e-0Ate!a_NwyvCQ)tQSIIp_LK z*XVljon1zLzR>Z4!a~FHGo3HWs%`52%!pg>zxd*j?~}hBDLhkQ^X$O`1>I}=f4|$k zW{pnrZL_XL22#Bi_WK_^JluX~?{7hN_V$$%6#A0cnjNL;H}BbVW^49uP*HHPbgsJp zJOep@iQZ%9zb4s~f2`bCzx=Z1^5yA&eqG~5Z+1kcK=Sx-J&hJ>ct$X(9bTJhn5 z;{2LVn)B;^-Q-@>_Oj&E^7(acGV1pCZ{A!MqX$}(lsA33e?O>-l)imCTB6O-Z1&S9 zPg*`!G`(k^{%xzp{>__<t_GO7oYt2@xwb_+SPF0^VqISQ5y=he%{{y*Ys>!cU}E~Q>VOc?Z5nTO5FPM7cT~C zJt>O)?o~ePWr^3~i%(8YRzI=HXK!4((ae3HEN#!H7(IOOV8VI3-R^qRRi~dWtf@J4 zcXxSwLF3O)#r-e7{`&K6`~Fm;nGY)EPMRL1Ydim%T1>H-(PbSN;7ZKX)tiB_A?)a&PbX znLcVe)}6k0PtV)i`$Un@`D-sr^p1ve;z(Zy<%PpNMB;vLjsLUrOY3~k@S6Xd^nZ4} zKlSJSsp9|fH8ZzH<>psbzO83{`B#CXtmgiHl?N4WN@oH@SYxkmTyuo=(Qdau5u=$r zGkbb-)$_#9&k1*9adcQ3bj*O~kwxc1$^PRj6`uY4`(l@$+3cg6bV_2qZ_Bp{?tA~x zLhtd%ACp!%PD?fMT^w*CSVyez?i16!ar0~J-0kMOU*}bu%(*x2(YwS*v8i6I20SzT zmY3|_>mBIbU7NN!GUvA2b<>nczq^(;=abE58_x78lzD!s^2DYy|Nhzie|G;*cHPg@ z|2zNwwPkBoeEf07$rLA}y7MeN-TQvs-v8J7->>!m&x`*mzbJNl+sQQJj9JsHHnt>o zyk|d?X1wgO=H-`Pyf(J#oW8Nm7u<3T33Z;f^vd2;3z;MdwpJ%O*T4dq>2d4ZA6ra6 zVl+u*;`!&7>k4DrPo|toFsRTC>`jz-^4T)Sx_Q;AGikNmgm_4%)~APeKV*G6;wX80^~2`ovKSN}VE_U`ou zO01GCWQ^wS+qc4PTB?ZaOzV>=fg-Hezs~Scd%5-VDvwDmt5&5LNfzylsj6e>`~39r z@>N>PW_E;#UG$lxvb0I$c;Sq*%T`8TIsNS3Y@_=1R{M{}_$X~NQPC94sh#{$n1dx> zjk|p9&78tm_3sjsRAMXlPq`B<66C)2Wy!BgKfJKDYU=+)OzT{itv>nWlX`ics{dCq zO!^q)IiAG}3NU^AQ~l*m_@CDIXa4^^s=x2U-){Tgo9gQ)>X&8-G|jSaSm5?SX=%{J zyLV&j{(YW*_Hl~Pb>YJX!OMGDyP59q|Np)I?Ec^7|8IZ4k_5K--wwehDKHBy3EywA zH*@Ums1d5s|58<3wU=L#ul>sF(tGv)Ys<>Etq5D45%s8SvqG1U_>|R2Eohoin(X^> z>n1eKvgq5XWa|3CHekK^jI(Jrbu|uZ$%|hfv$1-0Gy9RmB8?|+-u(HUy#k}D4=Sa> zf=%)~_8yMDO#xT-aP}NeKK6K{z-h4L2V3F)#sSjalUf#OJSno=bdzV-I$=QI-`tW>Tj@tLuw#%|^MI6PBGo@D$pXJNe{|h-s~>R(VYF5nwr!W_;|i;kwsCEC;!7pW4*3 zEHh-ew?`4Kg4X&Af=|0PYk3If4HG88DXn%;_^us2-Keq{h(w?Cd@659@hqTvS zcb%Pk`_MMy6|0(5JXst;NziEKo-JqBH?LYXW0|Mg`N$l@LM zb9GOlS<4b5#ct+^#a<7Xk`jIYdx6aKiy1Ai_w9K5dcv3eUS_k8ZZm$dt4+o8NMXwY7&#pI_%%9A6_~DDK&!%S166tmoaXt2(xoYn{@AK}Vp+!4$ZaoWI zeYV8Pb&|@oX{$uKkA}Z%*64asR^4+vx#qrlU$Tm)Q)sBpX{E`Y?@TAXc2nbFuDPFX zHP>+V*#ep8)_dzGrOtF(I3X^0Z`}Ng8DG3^XU%HMloIJyU8J$(V#dCby838|w#L+% zDMpbz&hz6>3(k9PJX`hi%T=WhW`8*PH86DY*Nfk4W3DHfNIkT8r)??cJ*g#8LP!6) z0Z+3=7ij$9_+;^G*IiYz^rj!)maB8xWm@VZi({W_)FyL2{`e!hFgi?x)%Ws==buwH zM~b+n8O=13;Mq6zD{^Zy{v3Z$z+{Undw#kqL$Xwpy~z1@{qt@f@jM@WY%ilmS5urx&C%PS&nDc-S_=-?ZO(J)A@fz8a!XmIe7D7x!#J6U7^?R z{SLkp*kAa0%E6b4ffj%L`Za%iI~fvwa{b#yv&;@}cD~~-Uw_~C*Wv_4rHHt78#iwT z?asZKBNiGe*?Vlx{P+(qE-K%8`)w*~zOQ!+_x1Jh$>rss136Vr?pSyFcXt^X znSB8gu_E8Kr*du2yPHzE(`{jZ!z7hsj}6y`&HnxUeQ{x6y4mc#AIjJ1fL2{>+`ISW zw{LtidwO+F`=|-~`T1Ge)HrRv`R5pG=H5aWJ_p1~paY%$XCg z-h7?Nm6Xkr*REd&ZU0;vBzW26OM?VMLpcvOgsnb%@gnE<_xIDyX2)8xo!nLp+7q}X#5FWD&A2^p`|)P> z@0RxV=@vY3uZ!Da^o~D!#+JYLtJv0CLYs6x-~V^J{`tGR{g=K!d4Bx=v$L;t^!3lT zs&id_*%P<^^xxn77S`737Bc&;eJqV#9->wB`I)bo*|n3;Ker^B)IWIIqwYV?Xs%zp zR#Q}DPHO0E`?EEaZEYnD;_s8=3 zUkwKzWEn0k+l?H_#k(sKE-v7X+m1Lf*oVIr+} z@5X{$nvzrZ|Mza2iVqK_ZriHUJ#}ho-KWX(A6OXwdzSz2_0-2db0?jD9uXa#Twg!m zYiUnvWW_x9_1B+&`7%Y_e_qT^zM9x|QSzUE+AuU9oZz^hRLjz39nXv3)c|? zpLDhAs;bTTr=Ss|%0E9EG`jS1_wV4_w$1G3y?bf7xuU+8I~H}stv^0l-M{k73($bt z`QK^Z^Jgs!)DQt3NV=meSN^g|<*zRt{dT{8NU!fjsw{qF)ipFpY5pz9`p7sHTY6+UY@LbJi~&EvzEW!?3gtFM>S`r z>yA4->WWn&9m7IJLcO2$J&0DEd~(CCU8l~S6PxNaGj6^6=9?;Ak9G#Tw^^6h9cgCo zx3H=@mGex%H884V?bWP3U$5!r+)gr^eKBim=VOaf*&DZSJ5Nj1(|h&h_unr&x)yEN zzyJ8LV{B8sW?sy2*?f~pNYLAC_S0u)rBgOX+Wr6YZ|R~!yZMWwVw39Y`)lmnci&wW z$KN1vd5eY!*Pie9a_#>Au}e>X?xQyOx?YL*Bp18+>9w_>^S6YKU&t{lj5QZ^%*$Dm zeA_Hg#8`r7#@R_UY@#?G^| zRpMasxqM(#&bs*h)4u)BG5dYV%xh9h<}Bz4+o@Art5!YOrPu1j_>}wlnpLYnnfKVS zDlMhAB~~thJ(;s^Y&*I&`}&C#BTxq_=K9}Lc2PT2bMx{*M{k$JCR@l9)zvlXoPM$E z+@&C;4}l`BZi^>!C5!dBUtb@eo|-z-Nzs4(b#GUp&gS4Y&=A1wx>JYS`{Pb7be=Z( zwaeu#SKdB5{j1>NjfJnRHt8^*+qSmm88-dI^UpJuIqr>Hem8IVe*VJ~ z+!jx?ItxCs?a{5R*&^MpC9(74U+X@ya0?AJ@lDOn?vBxOx10a`&6}QwAFjqE$4$5B zn|U^^$IZFv!12#`Rl{oPt3wsJRE#vlwC}r!sjR2R7y|e zkeXDz*!->B$!)r3=H@4#7w=ei_~1dsxqj@|zwVoQsOLen!=!*x;j^a_L{81vDO9D` zzbaZ|R-=mcN!tn$p`UvHa++pK8ob_G;9kp=K1n35b@q}6yvnoB7QMYC_q3=oZioHy z+ixF%M>(XWrQf`NZz$FK;(PUvx`yYK#q87G+&P;(KWz2UlP5KwKY#8L=wdee>9w`9 zL$pLgLXVc(wjEXs6j`X@71a?J8=Gh|ub?h&dE9#8&-3f?2Z-6cr6ur{W?1*CuhNH zt6sOse#?tfeJ*`|yKzm5Pxe-oux0n(KmJ|!YR$$6H-F@V*85c0JbSdrZ|^+?4kqxH zve!9z>yjm`mM>rarRwjNdY{WjHk~mkN>`Ku9mk`1{?;w6-Mg(b_s^d{e{rVN)1sNt z+w+1}t12yjS#qkx%J=z`Lu-$3%4s{SsII=eWOr`{qZ6``|X=gyP&?&&2; zT+A_BdHpp{yK|F*!tAp_ded*-x|LE|IyJ}ac#hfcsEP6GokU!D*gA{*5?`!;^W$^- zF$11y)22nl$1jgs`{~6+<#XrtUwo~)QP#F~Yv{eY-)nzb9QpG5Z+^}FWvjHn$0{BE zRFwNpq|9$`9Q%@;$=CQ^zs_E<>d?=hte|dY;%TRxEn7lBBfbVaakuuhYM;9lv?;A6 z(PR7Vryo9W_B~#<&NMrF^_n$0m$SA?@U_Q#Zc_1=2UtwccRc=KUGL=%8E$+qpF2=Qs7w&oszufk@&C68oCi0{6ci*B#GtJT#`XuH~ zzw=4$a$fVhhqg88v-bEYR5*Lw3Cb}uH-A}TB_l7t{LxekznE~LMpwbpn^}zrOLLsB+r0Y0p|;Tz3wfaz*yhn>TX~A9n8R>*H)n@45bEi&m zU0?UsSc*4ut5Eygq>Ts8o#UG{r6bku&j-E9CnfIlPf+Q6S>m<-{`2GZ`iEQ?xu zDQoNgkPSswZ%jR{`st_5=9@i-4?D}*RCGMATaOCWRkfj=ih0QZ`Fk&E@l- zZ|8cw^x8F{#B-k|z-L5FK4~=5=f{I){+C~?GIP!+7%aG(7p(DQ)+>CO()bG zdu+kC?)RIBS<4>n;@c;`Bx&8jgo@1Fu2cF|Jc|?W*loU%J-^?}l51a62ZI#m2)~@;Y^KX95nbzjel_7@;V?c3pH*fk&lg?Fh{M5fx z)z*F6{9SXZ*P1mt!RyT>_}cZ~cuz~^Ih>H1+8UMHxp{N3*HWd^r%&te2@3@+!oB)R zXlYOuV@Wicu zpyqjEQ&VWnp%c?DzSZ#%DwKF|e%X%2cJEe9xB2?#&gAo(7A;Eu^kSw>lv?{5mF<56 zB_8;@S4>vCSK%QNC+oN%!>TNmH8L<@tl?;lzR-$@kDH&Xg{sB*=!-q<*`=xF!N z#TLzc=N5DuXFT>-XPM_MV<&uNMO*88pO5LmYm!)%!jD{+0MVuoO7HVpnHutm8%AM;b}D);;!}?7`^3n)^xxvMH_yPl!!23^aXYpz=i~ZLitAk||uBA%|6>Z!7dAGo^Q}S)tL^ znU*?n$xn;AlRxSuPu+1mac24?4ZTorr5!fn`JmSJhvk!ZCSTQ7@LbW>nfKhFNLA#n z$8Vhi**`BO%C9IFq`kBg^*yc=62L3$8P>rh?|JdUTb)Mh!?Ss2@>(5f>|e5CMr)DL z^tESd?l|u`*`eX#qB(1-Vu|drwZ)HKeC3+9{#1_H<0nt1gsndM^QYzFix#?h)$^wB zD$wu>ni2b_SM#*c)1pef1uvF49bk172v~1^;%O0RzOcs5U2pp0`~ESSHs>F|c{4|Y zYv+y(gPA@->OBe^lPrZ=B-$F+z1HZOw5#LEXG@;LOK!gnT7Nxwx*)^#uNL<9?vp%@ zTCRURZ$pH4_fe%)s|pK0PVsvl=_q~Rd#zNj+DxCezkh3scFN3Jc2;?^j@MEl&}7Zm zm*0O+XJHPUvPa+YT#t{M^Ujzwn|XVtEl?AzR5>U5_~VSplP`O-mPSsK>^((c`!3y8;c;GeS(BZey(Mvqo3L)zr|$0>=Ra@Mkv?p2>BNd7GmkF{TWu)UKhtN~ z%Vrl(mYYJm)?Lin`r>QV^f~^McFZeYb(L%PZtLQSfo%7`b2c>`OgQlOH~)+8>|3IA zYwr6$pZflHkK4?hE3yl9|2$mu{Ez|DytT2Dj>PE5ES=97`!rxvhmO{C&}0(7oXwLj zUrhG$%|BjhTXX;Tw%koOWfCP8t-62KJ-c(kA_ueCuA%dLuQNqX(-i9#;i@d!DYI(T zvUS#p5(_o7CVi=zU#{8Jlsfat=hn(Mp+dSMZHkjmwp`4Z6Sum1U*sBAhQ7x}Gkb#7 zweEkP>9dTdU3uEHX$HQJThBibGfY_;G%;zT#s2M+Iu~WgRhllk{`z5Moz;KinMW$U zCM8(-v{YSwY4W#LLe1&p#S9bi10j-p?%`ZnIk(etbEoE*H9y|5<+iSfYvsxX%Ys%W z+!nk1a!THI@8y^Gifn&gDR=z%afTABShdNSzpNvt8TvNI=C4!Ddu4&~ufBWFk%mnROzkZk&o1&1aW!-lSW>@(3nKSNz*EU! zvA2K5e$N%FoYW>iyjy-hvAo<|S4>Yg^yaNwPu{%gY3G-JSgF_QG%^0qqvt$q&TfkZ zXZon=y3~HTcw9zC#%T804LXyvOgFw-e>F>l%kxazV+$TOW;t7{2QOZ9T+C>B|2y;G zt6N)Z^X~6E`}a40hKbaj9ffm)R`#sR{wy&;SkAug%&}u^*4DQ}R)_L9t9L2+`T13T zdg2+yQ-Af)Ru#_^yXJL-7*!fwy}D$>)7@u&<+i1_x4i4QP~Q2>RmF2!Aa8e0?%X$@ z*k-=cS~XE*7t{5`OY4mD<{0qQ{JHsrHL*uUU1`>yxrcQ^Hk>wPs7Y_SoPXK6&gDgA z%f8up+MYsg+Zv|86B(>R8o8opP8Mpg<^M0_?Q_wbEY4;w-yh>r^ za0f%3M+ygK_s#ek^`&24aEG6gTUT<5>%uL?!m9%kFYR*(3NbNm4vlk~B+7JsVf6>e za~rxBNk0kSenMyM!dYg44<7fnm+;vsXU%!wFBI6Em9`@8zaYcqJmygIiBd@`D( zV{87;KThJm{!HJ`g{LO3To)K||FFiCNo7Ce?xmdAa@*(IOLmuizppNuwe!L06H`xI zGt*e}eqnl%#x6$@wu;4!m-CwceNX~c>LoX!)(1BlTx@^ZFa3IE-E@zQ4`5C$z>N$P&xDMZJJ`;J;zV} zGM3=^`RDWb&P5u%O}lDhGWEJDpKV#VDocxn^Y9VHBonEHtFm?-(@zTDbvI9{_0KL9 zB^K%1+d3Z4sM`Cn>4s`wvejI@sa~w7H*EkNXEei4*~ItgCY@Vv#U6kB!MZUqsAK*E z3%;8<(}GsIY`$5w*32zX==S9tv-lr(H~*`?rXKiZjlRz5B|$3}WM;i$du(xz`}g~= zRU+N4Wo6qo>b&;8EqvVY@y8kdc0WyQ{p{*XYr0?W+Oe)$^U#Z*+fHw?DZk;_qSXDd zUa-@3in2;mL()bU3AUJ+D>KjP%?}n}DSjBPWE0V`nXUPt&Ap6cM~@zzn(M_9`{~YC z^;zAK_iETJ4;!oq(Rxy3xk%&Dix)G}HkYQ}K5UR;B2`#bHI2(I?t0~u&_I!y;4PQe zHooPNultdxwIXuID$qd6-o0mU-O}2XC*Uf0H?KWy^VtLghrqbl0A)|1UF$$+%iOw^ zl%C%1qt?9W#E*UU;v#Vc1qCM(3_Ms1&U0^mY~d##t-0bEj zmzPh!dsmjLm8omd8QHvFbLP+g{NiFVTQlS3mzz)QDB9Wbde=(t8=oJ4*rPl#H8Mm? zR8KEXWRE9kyy@|;udg+_d{$pwa{KMxFT1XoOwBUweDS)(YGpv+-^w(md)NH=sQ#sD?-q$o8e-2rSF$xTet&n@eVS^{ z?X=|NxqtuKuDZ%K`DDxH&Bk)}bv=m^B3zZzrcb|l_wLgB@6Rh+r>!sfSEbgU6=88$ z?%jgZODda|tmu^X$?UvyOK;WGrR853md_W7U7IvDCwK0lUv_~Km+UKjUa0nQb>$_9 zof6reuas@|_?hNqzsHvh&(?5W_35+Bj5n~bb_q1y;={K(Wbuwl-m_;eSXXt}ORKXg zS$9XSNeZ7P87E_GQQKoJw`he_)jpqP+xjNzDy^26bq6^0* z%OuVH9H(vU^1d-uv}pBhk1nV04~!o@k7@3S5xlx(#@DEx*~`E5$*HV!T;{uMmseiu z>(-C;l3mLd-n8tBHa~IA?8EZpU&s47(~?zO9XA)>c$zA5*5Ukjn}nO2ijmg<>>zpYo4o2Vjrd)tK6R)dn~O8pa#w7e)1 zw3>c;%s`EfznQ|M0{k3%)`b@Hy5GE%Xv49#zWCnzo~}_GA91BMZ+Q zF!2Qs{^Y-HoZ*^iJ=ybDT4`zPLr?*cVB{I2XX1N!lTOY3bhF}hB5QQ+IEV?x&RVwA zscCgY&i_AuDr3d+i7^iyW@%wo?ABF7EY)tCFI2{$PuSj}C^x3_Cil7!Xc zjr0HgTV+-??|6xoYiKCX5EDs_1!9bT08rH9p-93^;Ae&diI-l?-snaVrOSxdE>kRPti^p^Za`X(@z^p z_3qCqXZZYLgVgr?`)-#_W}Q_#{BVN8hd`03OP6X{Ti;GGGV`Bj0~*JfU{Nu5?%auI z(^iCOuUWfR@VJ2l&xSQeG&9S~r)zL!n$4bmjWaZK<<+b`-|zK5`&=~1ZNkY-XD(e5 z+Iq|AY0*s3Z4v7J^Ojs+o_o9J_~f0^=dS*>pM5sSeWA;QPduKJPCP9NSsfY{7FO}; z$%N(p^B+~}%U4JGwcI=T%W7fJ%7B$25m8Zx3`!V|J@%BkKV8Ot=P?uC#X*{&Rl7IJ z+CEksdim1w-5tgJ%*@WDjVGe>_a1%tP?2BW?$@~^QJ^6uXfL>;w639P7VF8&>D}r( zQzFF{NkQfj?JdMu&uVnt-?2m8gT;Q`>!PxF~}QKO^XsjDg0ow8XHw6g2xPu9P6{Gq;G>Yc)VjYT`> zSjFt&+O$aNB-g)^GfQgwj9)06BZ*U_HQYSg+lcG+AvsfQ_It5c?%2bM4|4^FQr zbTJTjTM@<<87eGz{F9rI`IWsg>s7L?KHZ5;xwP_eU)70KZJ}!xt*9z`BidW|dd`K} z;wLw=_QWuzu$Es*zLY88t{Aq`EiYC3rrUKzHqU7alaFhK6u8?iGDrw{-+1NV?4AH# z>D4=0tltQ_3$DtXymoD}sP>M@ijS{ZUiO=-@!{fImnXrub~c8tN(v9_IK)tXCH+?J zg7i-jOQpg#dWrmKnb#g0c~DFDEUR(poy12M7D{z)b`kmU<8t7o`+xcVmGrEUOqqV| zL`axHk!#NOV{5KAJ!dhUq@t*)^)KhplG4WaK8BlJ9~ms!Qp@aWKBG)(N3-1HYmrjQ zOV2%?u5^E~dOhDm@#FEw=J!c%KUeVU+u4Z#NgV$z~ z^XwI8beFEVB-E8QVWW|LuikF1!v-64wD;a?Kg_sKz9v=Vwv)$an@u{4S7~W$YhTIg zoP4>!V$tolf-H%z6gG7<7sh@+_2|W+#3QA5cRIJs+jV!(n;o;3ZQQcu$h&uDjsln7 zmIa6Fg7!5Tm-?)@npH8++it%5<&uz4#o1@i{#HCZS=~Q#yY-HB*8SCd;L$dA7|>tgV&xy=-A+ z<+nKc!OR8eAMGlqCL|TsIX*aldxg^JPt$k(tGT%GQR#{>?I~WWJck!VtqoXxwLoUN z+hWJ{*AJK8{UUMr;fL0W*(r8f8eOxx=C14NudMUa>MD8~(~zQfEzA4KXI7rW38_on zR)#FPFAqAmnujf3)2C86Kt%NRwnmjrFMdj_p1!d_pej_wb5VfCl5>IGKbEX_7Ge!e z%Fh=+ZS?XxyG`vZi`T4<9(MDSv$MOeUY&Zq%fkQN#4t@U?k`ofef|9}%XU}HGvA+= zn4H}F^=qlxWKDH-^_Ikr#}+11ywj#?wHP_8v!@fyp z;_1}Ls8yS`ZS&fE6I29z{>&Y7-7zqv=6-r|a`V}cgZkgvmR*qTdnV`8GaXRJ!wJoVTU*T6I1%8c@w4>DAIIe+M(CuGnZ zG{gJv#!p7>ta*PGU;nQAwQwFth=u7Of9v!2twQ(CR4+`~z|RX?3$R2%=)o?}z-yp| z9RC0Ry0SGhK7amPMpo8XhVOEc%f^V5;^O8SyZ*-(cXpT0Puo0G%#y8HvDZ!b@|GL7 zZzq?QPJLN2iz`b-Q!{Dn?cBaT&}5!VV9WELTW;qXNzQg!=&=6!<42E@>h46o$_dmG zJ^B3emfLT`?Y+`YUp%R3>3b)NQ$wtel!uP)lx_87l#l4(>CvfDkbg%uA)98~b zdiIpiFS$!K_gK&Mc)n&bjz6RoCNWQ)_0E(3sgDd6)c$wA6KG%9)|tHLWXH_~&GXKx z`WbBH-E&eP{eo{&qZO~n*#%o_WzTNnw4Lr#DtdNHrv0A^y&TV9Y9A(9=;RwYg@ydN zIYr~kQd|>n-BDrO@~(0oyoay ztbEd=nbvO%Gv_8f_0E`fRy;I!;roYYkM&3`_lXkEj9z>|oy$4Jb#c{OGq-go*4#ff zKSpn!eScc2M$Q{WPlHwRm+dQCtmV`mce^||%P~jN+x&fk=;{|?V)aH!1@U?-42xJl zy|z9Qcw$MdU}Zfp^aB|#mAP1M+IF34bR-o z{WRmJk(-+T_Ya$oe9bWNNIIrcz!t0GsTLghH1!-y_{8(iBjV$q|N8oROO&tO-!H~) zivyz`)XIX_Tjbr@aqiS9qq%<5(>8l74ar={eQJ8Ale|$e;bP>9H7B z>rUfqcK!151vNF3+~(PbR1W)BJ6T$D0y9Dz0{}pR>O00`p(zN)f#Jl(J_ZRHBm~ldVehp|YQ=cP? z=Ws%4X)9=ne~@1I1eF{qOt*KglkC$D4e(31yRF&&WD{s90Sr-yI_3c~TRad#x z)t7I%Eh}^Us7d_2^pj;$N0TN7tvpgeoC-nz!rnOCyBZ)}^puN>5wg%o0q4}L%Gj|=$x$=+eI%yR>t z4}a6;qqqWl!Lm*AOr94tJi5is-)G*t^SkrMV*kI#!|i%`9{jfJdn~{*JyyNMRuT!bbl%n2_{6F<>-^PgG9sR)irhUU zSt7ap+{`WKIBmr5&lGA;6q^5}I&=4;3&-d88gQ$I9QNu;-PJfxS0S5iRbt@Ye{u`o z$62X+hJi-FUa$!XZCbT7_uFz$mWi>k2em?1zS-+5@`Fdeub@r#(DQS%f8Oa@ED&kF zQQK7HuFJ)qBdMzK$JXhnC%@XMAERCW0-6Ixn>T~T+gg8YHoR9dN5uEBL{dlRhq;>XReG&BOQe8j@&tZkx zXAR_{S1LRyjS$%~efsp9_wO%_&@qtVtNidl@obv*-hcCVrXT(MLtQd*UsY}FAGt@~ z6NBQ981Q&41#JhJ?6uTq_0>mDp0v0v{@64lNiojS%IevR7c)M8{(MC<&B$}kZZScX z#~)|-EpL8b{iVv*?*E_SoZD3oe;j@|!B4$#O+ct>?(0SV>wf*V7EC#jIbY|N#i4h;<|;qA9)O)cWCDSeMYGac94 zpMR{KU$_7K-{0S#uhX)cmYRH9?ET*I+0D)BbLP%{_`6O^$m`tOr-8wnbk3judviv8 zxa)_{pR26eb+tPWMa9L<+wPxaA!G3S|IOX&pB?-3#plEMk8JY{zNnonS*y1-D(Yp@ z((b(xM{C;d{w(_Kn5J*GMfvEKG_@Qy zym7%{#{b8Kj{HCPdsn`FdU(HQpom!g2KdaK{=(dC%ds-X8^*S$qq?ToI01tldM zu7MMltxt%&ws-Bhe{+^(Zo0JW?cK(0{tGj;x{fL}=tz8X?pQSAeRbrt#7MJcmv7E} zZnN!ef`v>{b@l6J`Q0H}i>_wrYVFEcas73+y*Ur_(<00K`BS#e<`%p9?f`WXh+5&)nsDF>YnITe9xuOXltT`zQbA zB}LC5i@xIbOG;-g`%o)C@455kEn7L)o74thP5=4s)zznAw>$4`<6%k_Hjv+_m)LM& z>$@#mJSVeEe6GY-vm}!%FjVn1+a!YqmGtC38}`D2?Xs>i6&+%tK`xPvOV+Y$Kf2)l z#Hu{%uS#LdkzN6PCxcC^oTqHo;+(Wf|CVfI$6|&>I_Ya$RV3e~FqGeEH?qB71@#`vPocq!h;4n*Vf{5s( zRd*#XYXwePD51)cv@O=vGmiTuM-zji$V4?mr`t}mRNnK>>X^h7X)NHbsC3`8YITs0 zkb6gsxTlDN0N2fZQ>Jcp@D;O&vQK9TJi~08DWmITesFf~&YC|44hiQKHRbIL_s%&p z=WN=Bx4+jjzgIqObn0nPoZ_)UnG&nmJyl<;)+(Jc+OcEJxkal^MK?>HHuKAzwN+=A z?Xs(@M0W4Cwy>~hcwcQ{Z5_X5#_V->e)}>w39!V($3Oo1`udFX&woDlTOPb(Raey7 zwxo?6Rkh5EG?X};dX5WM+?)SN;LIwGQ>$jRCU#g{6VqkhtfjVUmEJA+wR6I|wFFLY z3%@BHrL-iI={~<;oAR8wa~1m@yDsfub1~$8#lu!xvU{)VR{6@nMs;qzzT?R^wzaNa zecC$tOcd+A_=7s!SA0G$x36VCY>+TZEo!ZAPHyg_mzS4+_{;G5r%wLGNn5#N!XEA1 zeV@g#VbO^Rsha%Ue$6-qV^&+x%9h?UI+#lrZk`1RV~@9b_ChMu{-PL%6)qO0SHL&?)p4;Ie( zW4Z-&*6d^V8+URXC%Md-x9#Hl?}?FSC9%$nFGjR@zX}Kyef&|y-hT6f9!Aee8#V^+ zHb0@xp_I1yW|Zi%oZ0P_ulH_0sdd?-+br(prbQYHqSik6@PT2~D$pFt<(CIO@AzZ|*z2>UK!r%8*aj z0=gD0iqTsbqV-I4VZ_3=4iI>8{&2Y9ik-T%6}%=cJKg{C(b1ka^P`oNlork3oO<(T zj+05<`zMUH3#J}8+OOuh>Hbf_Vv9qIPB{v>cTclXN^xbLa8I7YOZdpeyC>h7+wT{h z;~});v0dK3FZL7f?4C0rw3*ZS$2G4F4gw60EedJo3f$*9zK19TlnA#TGt>I5Zm^1X z$7MyQbiQ&ccNvxmrn1l6UN2s?ux8hqO=sUO;M(wGep+PWXJ)4;%^(#|mU^SMyZhUW z->hR@Db~-Wzfy**5Qa zven#0AzDtMTWuByxC-Xn-e(@MlZRdJe_zVOBOIG_76xb>`2IJ#QR>N6lTBi-50n1h z3t@ZxHP2wC&&N+sHBEe*b-X|PRanomZDU|_;)}|qlh^PjeEWX-^Sd^->B)P(&xl+z zKlNrotoY=UBFzUGrkT3$jRS2WfA>x=#(Z_cvt4p2k&!+B|8d#u%5w+}t(ezu(DRNr zv`%l|dwnMsGquT@zy8{ZxWM@C z^{#c?_rEK4DXC57RGZ8h66(4$=HO)YenYu_!@2wR%HDmogCS8+t>U+#6?cuqP{C=(MGxA6Ac2hWz(zcy_Bv+;h&vCcrf=^wtl6y|AH-c$3FD?L4Z zgN}B6pXJj}%Y5IPYw0 zu66fkg{IfLDwodry)L{-;ebJjMisIsIsflii z_TM)fEZ{$V>X6sG?R9tK>p}gC26U}BP8%QW| zG}%1)@#f~`{@aD%D%RzTYprSHtlj!Gsfn|$it|oiI&1T%OLHQ?xoG~?lUuHa$;LNZ z^gXcPD~WB+oK?BH`|9(K)8%!qceVZe^YC^^M;F&o4W|j$=5OO(G-IoB&{mxjLM62 z=XRZ2A9S4MvZsb0*{$o^;`(*w?b~rvHm}n36k+Ht^ISMR^mONAfqyx3>i2%im{7{x z&|txH%}J>%upuzl;TkuGE&I0x+>Rm%X4@Y0{l582U;Et)wyxRL0X<5K4m2CT(Q{Ib zRF&8o(HOT%;p)46)&IN&Q$<%rHfgMFH(&{DStB*!YR)8k&cm*y^SAr!EH7-iuvL&% zPU(KD^*iM+Z_N8OI|NiD7wIr(hMjy-mOAJ5Bn#_zpr(P8fWn$yhbd27Rh3nDf2%kb z%@nwueRj(6a2e+*A|9)_0s|-9S!JPg^5#!PFZqA@Z;gC1Zh!mzzNTA&Bg1U=joY`6 ze*0#&NaIkqxPD=6?c8vaow}Yv5z|_o7B-}9Zk#=Pwsn7`j#!}9)CC$`Hs=qQSVdRs zcuopf9a>mX(Gj+~RmHQgY}!`InP;DsGqD!RJijWliOY~HC34^8E{|!d8ME3bmbIw=HHq|2aTIqORY)4VSRb=a;_*5*8-XHjkcSUHEzFS%GxS5>nz(LgQnEf$=^IDZ8e*9HqC%X zc5RsO-MsR*-%oAoN!r+uWVjl1-qf>V?#x+>0yLf^&pNNm!`3|2EXYBCVbhueg)t#o zq9WZ#%ft91WBG4y3tXFaTB5D-XHDJmU8gn)1?`M!(>X04+xRrMN#Q_-Nz-GC6<4!v z+`Q=~@Z&|vuJU))w)ANQ=e6umOUX_CqW70$Lq zomFo=EUc`W7M+mbbC=+0>wf)OW?5vzy6g!{c02A)P1SP^yso7*%}do}wr_}*sLyia zn3yezkzz5|oh8^brf(Jth&P(z?g9q#?^knPVaV5>K5Ore{W{P0tO+{oz$+*y`1g*^ z=_~Ax8#ua_?D(r!kQn*#QMv}lrZD-hzu3H%3YEmR>zw|%!e?lRc8wy z=64J#&3G!6;@hs^S-D#wFl^!URRKLp3<_UjSiZ?D%vR=$S(3@LHli_o+NLn?)6Wky zF&x!!)D3Geaeb2hc77PM)}}ls-LTCY`m`A{nK+nKLYw1OI$z^m;@Nd!qu|f(lW#5D zW~m9NvNkC^)9p;TdxD97((Ya|;K6h7rV5uUYZ zO6X5P7L~#|^7<3sAKtudLI`ix#w{G@RDSN8Xd)Z6lFRergky$G*=-gd6g%GUJEr2< zbYklSmxD?s!A*Fb9XZonE2+_*e8kMnKf1!p~-Q9^QoV{+c($dllEH}3GzATy5&f;RY zdIM;_bpOBC@rS=NmsrKte82mBhL74$)9RnUr!72rGG$iyOwFlY8q-fp^xqG;AmA#v zEYe|Xl&ehoR5`nCmyR2;Bu1KXwK8qnzTI%HpW#fOPv3027A?4$vmi+G`S;kcXDdRq zD*pUnX>IC!SkUsaWY+ecQ(E3LGE81+i1_@dYj{}j<>K{}K#`4`H#fU2W@M=N`N?E` z&E~54sohgmPTu;yG?VN6X9JnEsmhybH*{L`eOj2PE!2ep4jrR zpkwY_*__;hY4!`On`TI`vejth^lLsMoKv&!lbMDscF% zVXES~@9VQA*d+RogNl`%F-}{fB4zZiMMZmsh6d<}1+2f$$gue0i_I+Y(;H{KEa`gt z_N}Y8sr=!G8vKiNnNO@*m6W^~G}A64FQ057^LIsV^3bybV4IkE1p z01M-?nYrq|zduZ?=zE-Cw|#*Imx|{(Z9U!VcMUd~B`nKS@@@QiCu)ncu-I?Q)z6eX zAKl8%nxx`1DJA9VlS{7Fj*Om@^j5i6W|+;Mpu!or<(`@D>T^?BZ%q>|VK`7K%fsgU z=H0tz57%BjWq*2G##ArXO*+X2C(ikMP72VQo+uG1P+q*b_3YVex5^@H`{vtJ_M~lY zEZg0y(Pb#XV<6YRabuwK;)!#A&sz43ae3;C(K| zci)HCsVB^0J+bN2y|V87&2{gNR_;ESdiucA^Lg_(pU!^p>35E?E4Vb;@@_TPl{?#h z9a^lw(eaoibJkvo$;&eTz0(7?T6U#&_oVLbI8#;e?77S`MuzL9g_SqmHs3sO@19=W z$L{AE)4gAO-P9!rI`!%H?ToEa9g76!`mry*_`>nHj+cpx51%|>%+=P9BRSJ||x)hF-@|I6t(FiYXvU7ebTy8|8xU2zhe zwCdpe{)c_}3q-v&a%B&_i&resby>%}HN#Dw;l*k8Do& z4_tk8@L4mbl!b^iV9R(6BWDafH z?3EO`AyIzb^M#jR9x9#NpLL#fPo}3zml7WzpMs~5&*cLtMvfv!C!|WQ4GSw>5ux+& z)hnsHdHHK61#Z86^xQeVdG_^uzLyuQlB&7yzBK6AwQFLt&Z;#XIDYe{0e1k9M#V4B0ep-J#HL`74Cdc87 zz@;t`HhB$YyLm60Y}|SC(BtFk7fg8n{^L!FblVzr`0{f9gjsC2-g5h>P2O`gOzcTP z;L0iD$M^Nby=R-2s_40iOJ;2tx4U~gM-zj%_*;H1$(`%kSFg^_$;(?5qV;fgeqE>A z;>Opn&ndl`>JS>bB}z9ZKYw9})}vRixYoz*{r99x|G;cB+qd6TQzDODyeK%&rn1Lr z;f4&(X`zPichvsa+N}HV6LwvtxL2HHC%o#;sdXcg#Mv+y8NoOFXqHr^HHDMow-))Y^yZ|Npw4 zvHkXo@4qWeH#Rpjzu47wF{3AM`|cGkQxhy?9`1g>?|6a5p3S@}o^Bg6lCrb8XV0E( zVQYKz=~Gcqb)Z+W+xAM9>6^E2AD*17Zs03CeR_DuV~MuJg#3K*>gwG;&E9@zue@>m z``?<`Ir;gA3+K4bI2M!BytkjrQJ1r+!M(q4!`7{=_s_4JIemIK!zQz|=;+zquR9vv z@2_usSu!i$J!-93U%&jqyLpT6?%Q*D-vrQDOvj7t@1g8j4POJ!Zptay5#%-J?fj08 zju*>wEI$ez&)2%VhOKMA+D+>V$6u6KT_}~E>cu)qWnqXGIxO7Cyf&gy!84#lxN2vg#fQYNw;~x-Pc=P!w}apFV!*vxmaPG9 zmA^Ien^jgyU2&51lt{RjeR%Gd_uINTYmWpnY>hbA#Ql`FV-wTXfVQwzN*k7KV86~W z$8wTIlg3F78Sbc!OlxDB7tMBfzGN{AxL7>4d*jGHG5xS)nc`}xQ>POyRxs4txxLxF;rDe7 zlTF99dMtXt{Js90z0Z@~9CIyuvY4wNA&$TdJx%2-0d8)ob zM;h77FCE`g&9^ncZQ;JDTenBaJnxEYS@No*_Ir2y>6A#Pr9sC|p5%;LyQZsY(TWvU zvkZKT-|h<(NwV8sPU}+Hb8_!GTn>?uky#L;rRW>HVpW^hQl9(Y3kwSy z9~MjrpLwB_cd8d_b#?WPd-s}jPG2aMy_&`Pv?%lKov*d>n{Vvl`mDc8ME4=)^>ROlkW3nfpxI=9Ic@ZTPL2_WSf(> zZNVz3Lk12D102?dwZ5-@@ip(l>$^4AxJ>uSCs@r*vYF=)7?P8ld&r>V%Inf4nOw2g z12sexJcZOIYuy=T%Hqbntqm0L5Ezo~r?Y*S=wR$O&ePO8_+YpGCQA78-ADGHvS z7Hx79y!=u`TwFX`_rxYMYvUfb8P^N;+Xy#iMzTRNBopXi%Qqg*|GpjCXXr1{b#w9(QA*f{@bH3G~C4ZSVpJRQLaPgg{ z?}PKlFKCyBtoB{eYQ*B`^dMqJxjtiuMy~wAst+QQ79E()`>>B)BEE)y@g;}U?}A)k zb&mvc*~K&`OH^S3HfcH66m$r94DHQviPPZCuLY&o#iaDQEI<#yiH z^{h)Yzj(=RT^#Qa6mq6I*W$1ollJU@GB?{LnX_2)w4`rTe33bP`TpVAeFjxu%=Z6i^b9|>sVhu6N=hcls>ywbD?pw@jGXcrTq8)|EpngzZ#hFZo!2+ zjvF^NTm1QNu(yh}-tO3sKh-SXWTxIt7U7%caE)8w63fEp2j=z}WbQw*r@HY(@r13r zg%rGuR9st^tP)6-6ma3_5M-5`;Bb`!@wmsYw=Y3BC_|s%FgD-(bA& z=K_PXL9>=^V2@>4*V?=Ixkm20#XsMySKvr0llgAVG1t=RdQ;%q`o^{MBezwo3^{e< z?%l?9>CZ21PWS(O@=0jl<3*Quy6v|=;mo%0{o>niAMOADU7%lH{_%qc4DIdg3=_hw zE$sIn=oZ)iRHn)xHkEnSimR(M#5RA+Q&*3C+HpH=v*fL}W`_(8mCR}l%gg&U%fYt^mO7Hb@;uyI>{`RM8C%d@Y} zu@gUDc;RKt;ldnHYwX1ff!({U4S1L*sT?f5dvs@6XW8#h=M4WQOZ6WwkYV=}di>#o zK)?KahtSX?g*^`oTJpAoTEjD*?bdEud+OT$W{bYUii(ECi=AcU<@Hw;s7my!@7VY5 z(eC&A4rhE>cJ*qC(b4Vq|M5*refZ)9$L!g8BIV)Is|7c{->>igS=0CY z{QRWqYHt5|Hf$5NE|%h9`*C^xKKJF9PtIr7pWeOb#k~#F7p3T~nYRDFoP|U0ttmI`a0CmnB`FKNs(*D4fM!xk=!PM4*UcsHu$HyT{@C zf1UnQ-g`Xx%Wvj6^XD&)(fhw3Iccj_=(Qwm_P>94wKbEkz%P*Ct<`w6v5;g-Y} zjjCeicl_u6gd8%t*>ZM!$77atuh;q7FdxV;Ih0@bdHshyYu+BLeZL`g+nN{Oe|t=l z5EsvVQFV?_AGhZ~%A~Ck5mUF?>^zoZ*1T%fj-5Lre^<&J z|MBnl`-QKqWRCw_Fil}%dh&-y(MvMBPHa}}FiP<~zNVPZ@r0Aw#ODX*`wRS(yY%I3 zOX@6@jvDc6XGNK+`+lBPJ#zGjmykQhTuY|vwiCq@*7=lFZ5FJm{}Jx8uifHmt?rV*F&a=<7bxr!@0~6mX zeaUI*Q@-D7{ASVj}u`RiPyJ#4b+HuxsDn*Yuny|9sqMvc$Ess!Y{vk;V^rsy!xWY^=VNQ+@7u z9D-YV8| zn-fv4oJ^N2+!D?&6!S7LH~VbvT*G;~tn&JG**}-<|Nei^dqelO?%FV|$hNFaS_Xzy zd!<*b;&O11N&i0IFTL_>^U|kJzHPdCX1C7my*_G_|5TqYIC9kW+P32+z9%;EI5R7ddUBism7M0~IzW*RNc?KV5Q>M6VmiVS&dVC04C+ znzY1KT9SS9O`gp+-%M`OI{h&=Ha5|2KB)X%wdz^^wvT&sW*Eu!UsroQ{gFl7AKr@2 zyzShF4K`&*iX8l0)8n?-_4MNY^&d8`S@8MOQ|~ir#xe2n%6*Tgz77@p5gGRBLTT;W z8z28NY`!V7`DTr-!3LS{x3}HiBzo=aS>8t$PP^~6Ez7L@P-3^;V6NYLn`fVDB>Inc zJeGL;QDWCR=V_Y-oFv)`V)-}UEYUUjxUflK^{LOlzP@(-t!O;|Pyb_ydCzsfU43$| zwlrEseaD_XM@p=k*JZybtA4+yep_9nhOXzW8E4Zfqs;?FK3|*p`KQe1pLcWr?bJPP zKhsCyG}o`c|4yAx`mwM5^yxP{?5wTZb-W`Bt`vgjC(h1iuJe$d|MZ_MUwg-*H;+Hf ziIFq@ymM;!j7YCZERF&TF26ifc;}DIWR=X1n+pWDNgdww#`?Hx*_pGS9i}F}=*|}X z^t@#8#TV!7&Yz3tH#fI+3FNqJ@>wi=-}}RbGCb|&)1&pO@9*P2%<=gr&;8o(vTcVI z_4L;ZupFG5E6&1r?6GFt9t+p?<;=0yKTn7YJDFMaT*hk>OW)%MfBw`6+EmYNKWcNn zIoI0bvdVfN(3rHuVvS=NCdcZ3=kGuHf7zO_c{P83>HMF&_~PtuRoikwOIsv(*ce)D zZcJuv%b7DjUguB|vqj(OGJi-Bx4q`fnUlBQRsa5dXzhE>?=>~oefK9!%r4zu*Lh*{L*ModdFv*$D6#~$M6Ft| zV<-FOjlGrMnf}#$QriF-vIa%u7s;BbjkCT-vSo(Z$4^`AH;Kh_l7q-*PVGRE&st`u zf3L4o0WEL4Gwr-@SpI2;v!8yRy8FQ3uu1(l7RDxxgDY|zbf?KznaKMJxNuBj@eE{i zY)BGO@Dfrv>+ob}hqHZtrb>sBN@SzPY5^C)u1JR$w<^xhX51&WDzYs}gn`3g6|YJl z$KFjJFC6|Q$SJ^bU`@WWcIW~zuM1tGFP=(GvS?H2m{7_bwUf>9M4#%~2*=9j9Pf7g zza|z0ay$gfKVHpuWsRT0-FYRuWLHIcO%m}GV&a&%tU;seXRhR$het#N1qEw8Pc!&V zI@0}I?KR`9HBE9Kw)M?jv*G)5PXU*XNli}0>`&hwZkIc!-dQW|A>^^DEh$@kk&3f` zs)NWUra75H%Dv7ifgEci9A$p-+rF`vUbM*J8m|KDmn`N@d396Q_G~IkuuIv?usOhK z<5q6R6ZcL`P%~Vl;P4b_5#o%VW;n#UuK%t?DbEACfo(r}s*%5eHY;GR$akA_CGe&*nL_zfv4HTo;PzAlP6ETo#TT)N0?1r_!WI8>fP#s$-x(%t46}B&MXKRF&|^ng7A^2hLF%0)m2q zwo9%)S7}msVh)uCQ@<+K1blA17ux&GS#7o5o=M<*XVq2>_jyN0M_mlhm712{PNu6T zN4)1cTN6{Q0dn!Jz4{u56+jwbF1$GXu)|sRUkl$0_+OqP9Pi@d@+I)coknm7LWmcM z1_7UyAGS!qv_R>L{>?uwFe`waghc!*J_MeyhN^;47rEH7Af_RaVFpNP2bzesG&M>g z83tx966Lu93Ji!*P;za;0?67lBm*;8nW5`hp{9W-ZHX0%U}KPosDne)i9Z*zJAgc8 zAG=A%d6J4oSCfgaT$Ke#2#UWrfR+MyH>r3A2~^nsWYUL(-9>&i4knkt7L~tG=8FqM z!w}SH2hAgF(&3&bzM^Z<|0ka{?0<@aqI6Naa2sRIeRlztj>kV1wj=Vmiwpk|10Ibo z9m~GxlGyP27awc1AaN_#uq^Y&ty@kjLz*Yl_-*&_PgBucq~RuW+5pZ{EID>Px6Wx_}V4JQ*-*1Ivq@V#AwrIVu_7wpU*MrZVsnz{`z5kH?zsLVO!&ZMy zNET#a>~)g`T}aXR|I7S;g6-|@{AOYxO-T5cm0^IhfGgvyWebBe7vIcrxV(j3x?6!G zAXIc|5a-!6;boaNHzJVo4~Y5V^^s}*_Dqff3SCKc(O#EDJSRQbe*5$qJ#`MJf19tS zUNSkmU%yL9#gj!rN6x-cVb9OGnX{C9gO3!>`ETbrN#Qh?h-;gMSGR-rwSStPleYd| zx9;$(ihEn-X|&bo7er zuQkNF143CH1rBY}xpU{vgJ;Y79~S&*zP>;1end~I{Mj_n+!bh6?d(}G@CdQ@mfN`+ zT}{2cQs8yBA&Ilp8bszjPc)ki>R}vy$l)kp5Q3b^K*>?Qw{e#KnxK^nu4X;_@PXlI zl3>@Q+)pN~K9?I7X&CS@r>CdSIIrsxIECGfkDnhj_rEZpMPuK}LyKNZmBg|uBp65} zn9XKn@VR{C*)z4Sqe6=`oc7;uPkb>Um2=gqg#iX2EfQ@Wmqq^mQ&=yj>?!0Dcxv?t zk4v+?Ou%i_gz|EBc6RoFP}WU4&)?20iFIEX(4gX3Aj9q`pwRcYKd#<2@YDUhfBr7g z&^@O1>}rNoFI!pp_Ko}Z&kx_^IZ0&Is)Zq1ik?CoO$~m_PlJlwzbnGDm+$S@Sa&~M z;M(e4wH#q7D0#|AV#Uuxe0<+NzPV{E!o~Wf%J$Dg{^cKP<=YYyii?HW*?)tEy`EQg zEYgmc)*7_3o{)y>QMwPVj7m+iM*C#e|t3j5EuJNWP+P+yL4!{7?CaYeKXw;zWn6qw;%*-Qwzmg1>Dbi$k*Kb& z{_!^dzwG+By-p%WIhq)Dt$Y0Oaq^q@?~{|0neW&CXPWEBJ}q@&j9zkfHpsqK6;FxY zV-o$x3u3=7zHa~fr+i|hSYLmwP4zbsP|s%H-*3?_fh`Igik?D!{ql+R_56!89)0;D z^7fnRBa4IQ&YAi7^%d6q;h42-;r;i@9PhR*0M*z+Va}`Mg;q^W{TU|hs+eEDLgQ5R z^!VJzCjwAv@#mo2vPgr==d#1%iw+9|4jetos;9sH!rN~G#|2X&kL8%PzuslwE4(dN z*)yqQQODY~z9v$o5z~54pVr<}^_6Q{sxoMzh{;i4!~Xs4hYvend0qOY%J#-K-Unx& z+{!UK{_>?JTeD)!^~T3LUX*cbOh3IMdgc<_K>*0RLpWae$T%3X{4X{koTW>Z#DapUmwNXWW4ztWS$1H?2|Na60om>>gX@)e@`b)SHzJE`cH|7ca-U*| zWSy1DI@5q>;cF?cr9y3o1OI(i;0S9n$v;xK=||nQphw9|GH0z1UK-@KF`^|=!hnZ) zX%MHhwDb%gwaV9B^1J@M&z?9@h0|&-ACL3#ix(BMwo0`fX7*Vg?4uUkb3EB+`DN?d zyWNLbhPN@ts+pBwE6vB{T-XUE}LwL(lwdQdnLEaa-Q}P5<6c0?io+f~Ok1ue>fbktz*Y9cnV$_sZ*1 zpXI@Os=i7szNo?fUjWoc%$#*-lMZObOZ`v%|N1GBmG6zDrA`0*z5oBcg^f+aB8{J) z{(sW{-=x5CfA^mnyTebPipo6CW3>QP!!Z2Igy%|)VxmNn*=)rwrzu{pBHgZM(mq?~ z_OH6i<+GXL_{N78dXrQdA6sZl^;!|O`ofm4+W4M*!VqbAbi4>&(w4F? z0=5<)lk4`j!n(S?lPNro74_Li@7|RKtx^hG-C4Ag=Qe0uu&@5t^8Z!&vya?fq;aIg zDtWdu%*&u9Zu$+NNaJz#Q45Zk)|<7pYwcQHm%yI7{rxJQpB9|DcTdl2sStR!r{#5( zMDH;Tk**b2vocJiX80}dKDkL`lg{F+SqtOVEAuFyc`lZ=*`NCgL*~6WFA>*_S!}V_ zKa2U@{2l^Y5B@dbwph+>HU<$cHLa;eC9%=jcfUAax94F#QrM&7sldS$656`%b+=f! zrc2l?9IUZxe{4}8<1i^@%IQ>(Ni3mP6}sNfKck|g^h@TFiK_q$2WZ~* zN%QoX=bv*Ne)B$Epk;=SnuWc6`(umvq8+fL38KHqK_l2|Zi0o(qKg?1PEI~<;LGg5 zb^m*!1lzW4+djOwxY*Sq1vIy>;#p98*KuLMfsY>z%BR>@On#RU~{Cglhj^d+Cz)Kj#x#cL_k?QMcAjEgi{-v7Q(D!Y5P^@iN+jY}H- z{AF+^&896&llb+uX}&r|2?nCg^UH#-pTjHn;1BR z?x=WeQaL?IwO@Vm%q2#TR|Q)ZFDoik`>7rDT&Z+j+Bw%(cPxu1S%f}T{dS`(HB!j6 zP%wOg!nb4B92S4Cjh|n9(ZSomqif2Oi8;0Lp}QvRn*aQ#t@P>@|E@gO24860BJgOo z2b1Z3^;N5a^j5!l_byN`{KcC$2a+}#n3f8Kh6akTt_^dykxNcj_tOwjntpoWDy>eJ zrn`4-dq9_}%sh9_@7C?xj2ALQN~~n<=J)@t)vv2>@LKw*^;(qP^v>gtS(q6A)Uo@e zv-kF1Z8-Do8{f&4NlTZeT3K0bxS7+_-#_tuc(d8jq|Op6SFNdw0#4LxKA-QijDxND z&HMM4ucV}=rOk3tYCQfpvHm{jq@SNP^V-_n*x1=$fWp$`#n&nmDNrl-&HMMkI%4|* zE_fhajH?Ls;(Z^X6+dmc*_u-}N1l3WBrBV{Wy_WYS6^vNUF9g!pDy2j+*wC#d4vwr zgtKWMfB${*xw5LNs=&%RBWu$l4X&d}E5*!i-nz9cK;ueZsD_A#$|Mmk*Gn(IY&7uP z8KXA)tWd9;$8zJX=TAR1dRfA@-@f5!(!#A*9fN~~rFz-cUw1c;SLpgbQ7q4A&3l0{JTH97&;j=L! zrTVt7+3dyp_sg%o>Xp1$wAW2>@=2lYqc?8dQWD?@(3z$ZpbYh=6T1vI$?i+`O zdM>}5lauqHZ1=_2r5asD7ajyD&Oa~T6RIV9b6tRjKxd2R^57jYdRwA$bL+1P`ml1f z9-3G-D?r0XYpUDgivc=fnp6AwtXFM2A;Qt>H0AWvsD`Hn`xgduglM(Kt#=O;IrUU% zrq495rA}&-6RWGG=bvv6Tg@mEveju}z>*lf&6^+FMMQVj{L2VFyLsOJRjZCXE}RBB z7QKAo)hvyvr?#@q&N1sgzS-YG#&Pvk&`Eb98q>9>oWA;AwA+>QGQvM_)}!Ky4rkv- znpy2o|)#~)nBl+xmtDF@l(>9;qQ@b-pZSqOb&FxjTAAX+SAEGtU zXPMmYX@?*5VBF`+p?GAv@8?i`2e=)`;kaIy^9nWzj*pr`D+EC*e|W7}vC3;{(88dV z3s#ASg#KM6u8I~R6C8R$)*cCW3SAhpExp%@R@n62-s`?F;K!=zYN!@`>{V}+0-4jX-oa?*lV4w@ zo_171!(wgWoCB?|oY__eDm3N?si4?C`ck8cTRcF$St>*Ie zx`AA#@#JL6CY8yapML%k&*HrFvSfyj+N;;ESFSc*d|moNhKUB(l+&r7elpGUX)Cd+ z&6zuYnFd$X+HWUjZH>}pnCdk%$IRVouG&z6FO8haw9!A_VFkL|hXyGCDS=Kd<=r z^ZAC`Z$-LYL06*|{{2-N78cge!Xx5(FvZBw+WPdFGbWE8C!3f?8cOt}7)h3umq$iM zKK=DoIwwDW^7-fX#ll>`5#Q$c;WticlUN-*T5Z>pOX(gJh9Q9Ij-_) zYFypV=qH~m@74cjuKRU)ey!?jQE1Wvg_*|!H*m1koYm}VI+(Cv>(;3oHwOC6pZE01 z$;n^7e7SJxQcu|Gr{CUMU%7g9p~k6+Z-4#%J!9s~#Js$|d-vpe+?1z!fx5CcbJ8+0 zI?kQ*n>9Q4&c53A=9eeJrdSubC`dUT|dQ!j3h zLwLBj+3dFo@>)~9SXXcD{PnBK%6fN3Uf!{^%^Npta8R0PQMON6`LK__fAPCJkych# zE7q^K=Qz8<#N6E2*tq%f%Oh!<3*XM@u*XGgd)e8?E`tZRZI9NSUgf}~Twaem-$+});yXPJ}=vcHdW$KJiuTfKVqktD-YPr2^i zjpgCvEBf~*(#qPpz(S_NMsCu{l(h?;7GGR&HOsR28PEOy|GrDe$*Bo-ZrHQu(7Shf zQ@vXI`uP0jSRA}@L*nn>T8UmZGcz*{k*+DHpVs%?R;=@1bIlDTJv)Ej$>Q^|Dsugs zH*P$6>J-c=dL)24mE#6=Plp9z5VD>)tGg>po3Q!=K7amx^JXFQRYSkIb7TM9od1{m*fF=0Pe1*6;r_q5{N7Jr-2+J`QZwh; z9zAiwVE69bf1a7|e|T$a_L0Y&T&+s0Rt1Hpr`6QVxgP(YFE%!|r?2nA&6}PoLMyjk ziC7!P#`bIGEGy6GXlrh6?wUUzb=Sr2c2k?|7GsK~sZ#?qd{%}y1v;!Ze^63pU}$)7 z{-2Z4U%r0*bS*kxsQV}fn{)hEO>n-25=Z1k;c3s*HE=_ebf8G<)2E_&cYe&6I#qN{ zn6|C0ZU3EP2^TJ3yy)oNE!*psSXkKEZ@25%pFg!-N0Z)GPxNRB(K`9#2TR%Rxf3S} zmX+Pv>d+gudhxn*KJoGL>V9(+W}m%u=T1$^mXM`EiV{4ZK0H)vbeQn{{{M#EUqwA>B;ls`rb7i+|*>d4R!odWCD|Y!=SxISWZU6q&nMk$X-v0i>MR$2a`}+SU zc0T=F84{YBkltGva(mM&b)d5`=)*S;<8U#?zzXy%iGi6 zfAPWvhn+ELnVB;a3@TD=dKYPwl$RTun7F7-R+He_xOZ>u63yic7dAe7mNsj4ZiJ2) z51aC;sD%Ls}4QUo<;-hQdF4O>06udggB`u2^RH`S(}PTXpC@y3mgix~>D z&u-fP{^;}1wa+ZW=PPRO-D_)NX0~XRR^ImC5B@FBzJD=mtAz~Tn{U&aA4^=!+B(Hc z)o3Qq$BH=)3LZLTH6iE47hcWE$lGRUYwNl*=F!8$uIplVpZm4z^@$3@hPQ8XnHm*j zW#4Yux;4>g=Ebb7{deB0U*G@p!7piz>E0q-u0}IW9u~AD7_@uE$+*R9!a`Qoit z9gB;N!&ZyV^=n_G!T#OAS81Yx1kWU|rSpExn&G21=N#XgGHYI5-W@S|OpOdXWB#4k zT(P5V*)p}LXxWm|Qb7)u4^_2Ck_@|!CgrNkxq8bZJ6l`Kn>lSWXRn*{$`GaLr-eIR zI$V@o7GF%xzVfn!ho4`*CsTX#-o2ksq_0}Hj&Ju}zv^mfw&upKrQ#bS-rV^6^X}H! zK5oa39h*6G=Cb|!=N~)9CgOT{XK{LFe!lwj(;GK!@=_46_>>-8np-6$B~@T$z2W98 zez%5|B3!JGKE5iouhFmnzDqXld0QKs3?F+~wsP8L$|Ul!q- zFY`O{-u}CHV;5NcI+tPcsNhCsX6C|u`{ee<@rV3B z=@BmsYXZb)PxWHmZ-3zG>gy5l<$^qHfuU1Ro)qrC>$dvpr3)7tR)yWTfB*Tx=HF}9 ztqanc+E=45!Sm_CL8gxtJ>R~W-MV#a<@)vOYv-MR-g*4-k)%SWg$5Thrno3M?u=RY zw_&c|@}KSXKmK2MSpseb{ZVdaJ(@JpMaj`_zWega^1m7P?7VsG&K=1&-^8323S3_o zn_gP__C}qVnVEsampy+zo!+u_Ytgeal6iN2{8+jE`O$9iiq~t+U%q?y?797a*81<| z_4Q9`Or&^YW9R;9|Nr5=q^zvsZ?UX8-*Yo3t?Zk~dE4(Y!orU&WcXgaI-@dK zv#h+l@Xv^4whO(@&moN=Zv!%-G_xIB?sxZ4psXkDB@Q9v9^X=!ngKH7jCm z7^r!q7rX1oix(XA|G)p|f4}#;o!$KZH@-!!op$h`V_fCa=s&0Hf9hAh-}}Ad_uKzR z_f~&C^7v!agLg+dh4=jVv|U0{^5Dxi2Qo}1`7HbJ`|q7Sm6K5AXl~y?<$p-ej+(Km7l{{Qqq4_j}56{xUcB zty|Z3?wsGfs@J~Ho}IgJ`Ev8BunxDyIyyE_K0W3C`Jnmqai+}n95d;~i$XJi?&E8pD;~GwNGrFv zgv5*G3l~bsaj`J%h|vSJkKW!cFW4Cqe`+^UH}lB**(_P$KFE>lQ7c1|a&u+p`gvQ( zfclmux;FpVZ`LVzL(9_e?Pd@*wqo*f(vwzj9C6{O3+8^%z`NhiB#>pnL3}*J2m~S^QH9fhr z_&GyFZ0ynJpC|g&U+PQR81dru>%}p8n|JSa*Ac5v`R%$fLTRGMqq5x_Z_ax0?%lGD z8wKb3mFpOM(byQ#Q($rC#tjKs*|#NSWkNh`CsS5EPuP6(#j97VwoaPiqh@7ootc|^ z^!3-El_3TaJQgy2&Chz&^q(bu2sqZSw{>blabea6|eS+i&FtoX=uFkwPl8{46W26@|O zJ18`qe_sD+r`)#bb8M}Te?D)2{LC4X-@oJJY$6g;Q>PYK1pSWl_vhE&|3^11E$zjt zS1NM;55E4=5aXV4Htp5x*Ol*fcE2czdiDBsR%4&OS7@l$(yU|;rHMz9HeR@UxA$(I zx>WDAcc+){pMNst#*Ko9U*9dh{Ib(U$$j2Q|M>rxm-`2TKC?D5A(6~-;aPXclm$-ZS`;)guake^D*!-SyTC~TFxor2` z@VH9W_QMB{%ijm3z>`ml=1WdH@%(d6Zf^0XC!Vur&61FlODit!PB19Z`4Gf=g~!0g z#;3N{_F@Ll_Uh?1cJ&veE+5KXUpqT^xgV%mY&JW!_O9#Bm}40xLE+)Ye|~;mXf=1s z_U(o5@6~SGwk_*-xCraylPyP+K;s3MU&`-H>vd~Bn6O~qKDq9txAuK{xonmD$`GxT z&5_TZKhMm{di3O^=awklnLe|Af0q~Nb@N<)89Y{0_vxKn-2cu_&R4HqWn^p+;9%)E z`p812OiEhXvheV&7VJaZfwZb zpz3AT)~mliIMzGcrsl_l6r-OM_;S5l^Yh^(h}}JzO&}%C*kj(UotfMEkB%N@6*1fj;yLa!NaypfVjrr}{+?v!!GTld=+}+vr_Y`y*r=MGJUAp2&!NmK2p2c3fe%*8V z<*jWq?%k6E6_c}O&-PTCtfQy5Y42WN8@c6Mw@&@|(Qsb%JEg;i54$J@hJ}UgpW?vl zurfqVuK#h-&KIvg;#M^u_4KXWMIub+a%Xbbog`!QjHRu)Y6` zjW1is@V&dY_xP<_R=x=bbTJ|~C46h-(wUc+r~jO6 z|5Lc@sFGZN`|k4hf_&|oV%-)pWqxyJBxGet%|6?9F=JIP+wYRw7Z!&&>%ZQbzHI&a z{(}b@-`(9^u5zJFB1ux>#qpOVLjA{Y-v0dHV}*uTcS)I*Wo6N%@VLsEjg5{qcJi-Z z>zSFEok-ifWsAw>>(`He`67~=n`>fz`$SstkD7hQ%{*&oo&pV0wn>}Yff_`YF7;e~ zsWQ<6)I{I7b*r!0?1OL1f;B|4-meQ=?YjQ@r-F+}2p}d@b{q^out1?Waq@<;1&Yk=8=kxjLd3k+LpQeU|g+)Y1FAfUK%vodh ztEacu$kNi&YA$HJl!vW@4Qr|NrD#_4!9rjEpTTPTc=<_P=9&y?oSKvF3vgHuD}FY-X+dJiB~n-Cwqg z8C$mTExy=sGR0~A^~onwI^B}}b7pH!_db$jcTNSc*2;a)~;P!QdTy}FZ}q^DPF3eZ1JXK(}oWMEBeY`zJ9%Q>(;Le z0+09Jj^%Z?m@C`U-(UFai;h(9wA0hqfB1GgfAgkIUUu`{7Y7>5e!ZjaFI)bek5-^w zI;h6DnKSL|*|o{fV^@ZBxGcJ}x4M7+-#7I_y>2I;7EL*w%GG+j|8}5A>&1(K`##UT zzN6wJ)6=4vASFd#Uxi+~e*NUrqOI?5=9ta#Q@6FfyCX(AW_Q`#^82;o^Ve7XtJ*v7 z=~K}?aq?Ar`&^V9mwtN6#`x`<&7UXg_D|Ptzn4<>P2~T#`~UKD^71zB*>mXGGqu~d zt#{nb3tD+ad3HCSrA*(gulgcfe;$edKhiEAmsnif?Xhb!KMfjIHkft5#iq zotcxfWcTjbzkXRwn?C*H&l-_#SFNdB&d$y?|9{szrKhXUo-O_7L;L>&-@frp^l0%~ z+7!2b@y3l4x8JJ*wJ-}`UD3(Tew~w(vuXG4-!l)+*IyO1a>DuNg*(j*X8M$rmoL8j zG9Yy7)2FFo;?p%nKBa8lseFE~-}1w0n;#bK1PyGQI(AHLqQ{J)olQ4$k_rkq_QoB* za6#bm<=`7{bzgj~>N=`awRhgOZDDb>Uqh{|ZdHDG!1(`1zx}g+f7w%vGDEgj#jcOp zdFb`}`u6=ly6vA|SsAP{Su^kMu3DXd&ynxv$E|<<=;-G;*5!RgJJ0NXzwh+=`oDY& z17vm*xi06Qok=bWr90L5aN#=eB|%(mFw3HEi6vl zxg)8jruO0IpEKvrUuWWucPTyN&A0w~yWeu>_17<7xzggcc;VW$y^W2GfBrGt%$b&C z7%1(XVjvM9!h1C7;l{Tn?)?*&M(A8)vUFSgapkq?J|MGN4hx=1GgekUd}mMP=5LkR z+1h^T><)Uu8Ws1A3k%2CpR~@r-$eAjT;3z0l|9H3wPgLvu4ePty@cV zOqN`im%kVsAAkPL8I$yLb3oPfk|51#)28h#ddd|YeS2$Q^!ycO)e^nOZv6{dDYEf~ z`oU#M8$C8g9LX`8cwYQO%Dy`WZaQMgb#-!+Pr9fGE#1BQb;6eOJk8C_E{g-34k|P{ z2+Ti!{#~9ai)i=JzBk8TzDdZ*5oupMwVhwS=9eXU{&Ix82y#u<`cW ze`g%dCSEx5{4?m5&JQ09WMyTyY~32TGQ`fbZcXRIhl+2?tarxD`}wo-Pn|qNX=&Q7 zT~;$^%utx=vuV$sL(S~mEr%Z}H!cp)IP>+j{N)=rPF%U-`Rn&@NlD47RcdqfotFkF z&h-=SKi+x#@srQ2TcUVRrgT-<)ae{xx+=CRY2$%^`=>#W(cU%q|2bouhxAzDZ0T9@CraYI3XLuQZp)roN{LnfU)tGjL6 zHjs+#+uNT#dzN1H|4Wta*|WYpyu7k4LgfWJ&2HShxiCQE%Dkx|TA-uNzP~$r`PQoi zTdz9C$Ls(89S0rG0+;U{>rJ8Dj(m~1e(iVjo-dsq_vp{hVhfovz4(6A^xPzH&?7)-B#{5M*@_4BG&d=$dny=7?As<({H)+^2-Y~uF7xlUV2G* zc3Md2(h#jcz3G7>taJV9H|+g1^Y!VpVwc5+d*bw+ott@hc`seK5O90j)~k-y)z*5` zyK~IC_++aZQE|%y0s`mr!I}TKW;s!MRM|_ z@Eohst~PT;AqEBo22U5qkckr&|NXB2FTdyWIqNg$@7tHv^Sfs(y)3!uri`y|*sotz zFJHV+n0#`AUwW2ee>=Z?(8>_IV&3`gYLgSwkNaB4JbZVz8q`mjIWtpD?UxUB}PU@O*eCtQdR889QU9 zWtemwPF%iu^JcLXhBMFb@bh21a>YeML}{|;;;UJO7BcSJ!bN!P=J&_wwI5AN%*YU! zefHax=Wo9Mo;iQNe}29`UwiYvfBTdlAD8J1Tei$EX=_yH(WE=O&HL}mJFLEX>BbGv z(A3+vZ`Z6@vuV#BClw*fYxjb+rdE7-!1(rU?w^t!KY zg55_y{;YZP_O0wspU}`vH+62`w(jZaxp4Wib8M_^+UA)dTE5xY+RV&dedErZKSJy02tdkIP+E=GgQwIf7cUB)n{R)- zTYq0e-1_IAp7QUk`l_{h_3A07<7;~ScgCpA^x3p)SKGk^ht*d%ZTK*uwUxE3?2eD0 zU(ur@oX?&;TM-fxqZhvRuS;m?mRnh?&rUvM*tN)U{q@CHUuETr`3sv_PC5Pb#*G_G z)~@Y+_bzYNtXUf(bQo^D)ip6Qd-S!6lcj0K*|cNFj~80VfCe!&xO#iTZr#4EG1Y6$ znl%%A)UtkWi+GipkztYX)MLWgH2LaS;ZBw6>fI@uC3)DGb#==?lSqMnPm6x8e1G=J z70+9@Rwb`~Q)d0NsP*gD(reeQ$?xP34LzA+l#`qL@tpO01Ix;$ojZ-UY~A|sYt_f! ze}DXCw2|YtwzmH9xAA`VYB#;>)I+mA*uYKsT{IYD1 z#^vO#VebozH}~%U_xk_bh&LCXuettOQd-(|@x==lE;JlWsCZS(%ffVLxA}Q){g_2d zm$trqso8WeVC~rzTdy9xaz!L6*K|iPZ_1I2FH1~JZ?0IsJz!-BN2^oI;fL)-W)t>BdQL+I+&tBKohTUM9# zaf|CEY>kSHjeUBzy#Dm3Peuy^eoVYA`|ro<%P%#idOi7U$<=y%`wf}C%|Guh>+w)= zQk%SV@#5x}FS9wMtG#|NE8e zi=ej8OjZw2X zSpI<0mxoIB?`yAlC3>%ZH2zuhud~;_)oGrKlHUtV3x%m( z!hG#FZhzjeZCl&vr%Cnqz4g9-x&9|}s+VfeN|BWzT-$Hge)w}WdHd~`FJC66q_lu0 z)Y6vCoHx%YP(*2>#zc=PK`TKkFmlWmUkq^$Z+Bejelo3i&Dyoh8@6usT^f{Yb1B1Q zhL2j$u~(l%!^8b|{`v8G|K7dMT2l`_|18`u&mU~#XEQIMx|)6cbx?)$t zTHVvvXJlVLA2b}h{rlZMx`ETYLAQ#Uh=XIp1bVXVuurOZ86c z>nm$(V+#!35ViJ6k>#CtppJNEPL9G%pG$Xgk`orp&oT2ZFBeyvJh8yy(5FwICbrFT zzPi<>(sotU-{+@GB-}pz{Bz~%)x@o4My95%*Iz$x=C`ZJdF^f>@#rxB`VBjFxTL46 z3kyH~^8Nee%a?uQFLLP>+9u(g@w0l+m>V? zvEk;djQsrNd-wJpJ?eVxdh8T0|18a4Z`facH(2>PJ2y3T>iqwI#FqrEJZ|^Bebx7c zQawfza`JZ7b23a$t&J{^h^RR5?b|uw{m~kr1vV^9jp=rM;7ku8{?CCe@AWd@V@Rl@2l6Zch>*6+yCq8?K9`k%V$seY0`bvDLPs@f8WnoHg@)tPm93)GI#md zr7?QfXF0pMxn0OG$;r)q{P}0c(LVM(yUe*O7HG7u6}oy}Ybw{vl39Cg`5_I=YvoZz)IBR6;H!i9p9 zPr7W3s7d+UdNgU_;>FC9Px=_mR1)j1dpP0LrAtEJ-`gitR<=HTm>5^{Q7Aciaf*@T zo_n=r<=^Eh>$RqGwK^@l)+E$_JTflsU{>ytw9SS#HfN4@KQFY9IdgXI!*_RgF9}+y ze`nLeiy0YtdCRtLoqEns-EO}8`s>~{a_d?5@=1H1dTMkuX<~*++wslEPn;0&TQ1yv zwCQG!+T@dyPNsAmO?vXV(rKZ9w)W}}tyHb4>h{v@jq6?&N2$#85$ZqQc{C{}C+EY5 zhmVA}hhODd8npA<8Jph2Z?|5+PLxHv14v; zCAFrXKJvKGXeLkBqJwFhFW$NH=fpKh*FXaao(LVWS+izwv^yVpTzKlK|1qghEzywB zqX`!lU$xt{^VwxyulV$tK5Z2?jyq$NCZBxp`R9kKy(*La_vs%^Y7EixT^LaD%E4D} zy0TdJHK}b%6FH)zZ|msk*%t23>JEPw%%}%i00OOK_RZUO@4tBfnAf3yAj9O+l`BVH zUfvwDr=oG=#=>{^_n&Wo1#zGY!zTy*ipMAJ}byS1QR_Qi}FckU=n^r&&GJ|4IzdReVggrAsbrHXT%$=<(!4r5m`< zmYlr!##`Nzva(0tfA_QKinuy1znr*zyMcv8L)?1hnLa{%?I)iW&0nqf_`{m<;yKfUx`>-TZd?Ay)4-!CKo8O)GTfF#*GtGj698Is)%)8J$5I< zL~7ac^?X!Nm;F;Gf&Z2%i1-j~_U|@U-aZgE;q-X`9{q zR$k2DJLGisNzu(_L8qzB$CfZPI!xHL>(?!hrH2<>Tz-A=_3-0aH*8BR@nI2$Sn^zars&p zud-~P)bB49O0Iz$Zoe%l*^<1~>`~eK2cLgtCEpGSUE3SIdiCnVIcAHmzs^2?{Gr{M zG~u;t*Iv1P-S+a&+FQ$`XWPlH_-%CbaUoM9!_OMVxqjUzQCZmc7{;HAuU=%Nr=_)-->aCrZk^t=>C=CHU4Q>!Gryj2 zr;B~@Z>NQ~LWFvcaj-dWmH)_JucIgOY02fxsOZ}g($YuMHVgJ1E7)Cr@@bKKT|k+I zOw6t?GI#IZee$`otgNiB$EMR|5ok?QUmxGEUsXFRJ}T{u>613Mv)i<6;X=i8bFJO0 ztF7zy&$ljrC&PW$hBq{nvt4=3`t`}%w_muEvp7WSVcG7Rw{EG;U%$;gL@Twd{QHYH zZ&C^hJ}Be_q~xFfd_w-EtsK9$w)UB`XD?pAe*Cn4JtzfkcV{wfS6L`FqD+W&XEe4^QG33>VF`u`rC*A(gc@%LX8bC28Ng{xOj ze)%#J)X40WHs4YBIC)Q8{@T4Bp`k@PWhQz|$uVm`nc}qk@>M6Du0DMz*%CmtU$(J_(vZ zQCt0R_Fh|lZS8E3`|saB?ybM~gu8s~(YIySzb(9&VKCDtW={oZY>j(*_T1|4&vq6) zRpx4Cx|uU=>QvD;-^AG1*d*oT4!ymXnx6jrgk|K~uwTD^RXpy!e)09!n0MQ`oAzs~L0u>JYM<;!Oq zr=Q!fW5=1p?cK+jjQQNP#r5VSr=+l~4Lh!1|5Mw{%q%N6&~5R=Y171X#JE?jdQ`T% zzvTdEY#Ulq|Ik9Dctg;H0t@zrlb-Q3(Xrh2h3F>Z;{-MiQJ%a;=Qo6A6bm1#Hb z-(UYXVcvOhj@Cnae!ttTqpLgdWJ*MI^m7#m}eti5nws z+`q3r`Q)NiTE4zvZEbB9_5WnTR*P=0p5D$cFUKi&B+2mE^Y_MPW?s?J+M72ozmR2W zViv`JNGM_BmEvb-GM6o1zB1(0ju`1XyGoz0JmlCf#Md4f8@qV#UsfyYxl>MyPMaQn zlJiodw_D+1A+I-&p&rrf%BwaL|0g)~g4-_4jrx z(pa-@oly7Di!ZmBn3)+_SRA->NAgUXuzY>Z3sX*lOiBchpl7?(X}WbKHk5)-2D0Z`N>(YR2mOlObF4M z`sm!;-CMS8GcqtZaC39I;mk8SdU}&QR5Zl8XZWc7`t@tZ9Gin*zKGP-HzaNR;p6Lh z_49f&HdZFan035TQaV2V{ueJ@I`nVfbas9*j&^1DLkjJS7j4_NZD-9-XT8{}c@G~Z z{`=y;|NO6CR&nd6Cv9|zjg5UP`R3(I(AfRdsiNDqZ@2$iGyUexn~C}P{TD9=rltE& z^jluIGiFJUrikmp#fzJT)ztz-o)+8B|5>w7Y|EybIdiN`n=fXlh;YgFM0m?_E~trn zSt8Y+4k{iE3=KikdJhX&mTzu$P;gio0$P!F>C&N_o6{}p|HRhC+z&ng$o-OE>r|f+J&v2>Yxy?yFGySdr4{O zr(43lGxm>MTn~y#{Jw4m8+i9VLhRCDk_J40G_fI}Ao^{qJZ1vRZalg&<_kIyl z_nVVYSO{t|u3Giw>(@({FFTi)o8P{DTSvzxrS2c2T>tWI>s^!%LfWqjwr`()@}%&V z?b{8Fj1JwK#~!-&!o7Rv9z0M8TP-^M^wC3yn7+TiU;oU|x?O1^M|O7ho*$36Z@evA zAhqnqy?ZOSZVu5BwUOgr7{D=mEob*pC$-6G>FLiWHqJUc;oREj?G+!7ii4UdVXLRE zUF!)tHC?KA%QmYSGiQRPUkWS^ZOy*Eqw=%Z(WH;XbB{k3>2_UoQRDso|BO<-ZJ^F^ zx__dH6#p^Pr6a>Vj+jjG2WLtYXXu)~i{`oO_{g-bZJaB+v@?`Z772hJ` zAoa&pg>PLF$1eZQK!KYjk|>+3gf+x9P{ah8{>s#Nd8qMbX6o+`&x zJQP~ISozDhZx23J+_-g1ZKB5l4X&`&Qy)H5w6eCom|;@$OCieZ^U1hQAl7yV(-Nj9-uiLod47e-Cm)pU?sQoM8uU8Et$*S6ZP1RE@bz)Z({KN|^Zw&< z(Qa3!Mh4KLB)7$#@9Vyo?)$#ieCE7)Ntv0QP76J1Yh@QNUR+XIdhtq1YGI+^{PX20 zXRdCRdH?Ke^o+AbI=mJ^mB6(Gcr2N@7D-#zIo>A>Tu8Hmu)IOO!)t2{{MKt zxmHI%d=N-ZUhLx|vu)CplPQ}(b8pex^9p}|lRfm%VAZNctF$g=ObO8NF`KO_!u9BD zRex{RFP*sc)8qernqILtZrSqX5;8IiHeNjS8FcdU&{qeR@MKvYkpS1=n8(inIo39JzE!sMF=q#M?4otee;<~RqMu&)hv&AYef z>MjoXREo2a{Ze(Z+143S{kBt z>C&a1t68HdaRR!%BHs$$(6_rL!ebNyX;s*xlpqI+GH zm6f+_v)WPc&?ai_w+H8!aJ_#0Iwvo$=#@N<9dZ?mIG6JNg6Y-?kC z`TBKoN{U3UTlcCkkLAWSKOTza-P?0?u5~#_tJBu+dxHW!mm9M)I!~>zaWj!po;-1) zKcY+VXf?cgU|$}hCB)|*5mVD~GbgFEbZUl4RQ3%GG4B?qM9>N_t*KS3YQ?%;gH~Qi zuHREX$7^ZR`RB^hPlH!v+)geiXqYxltjA5cYtaM`l@C?5J7Pe+lAAXRuOE`wpPirI zAEM=HHaj&xzyIIAx>v73A@8Gh_}n?Z?CjSz6&oB(r2aZ7PVrG|zW)005o7Eym_-jzB;#fr;N1pY<9(^7K;}vTg;tnVrCYg zHyyITY4eU9N1EBWRVJTQW}K3=(Zxb0De<9V)n2*j+08CWKTi~|4bgI092gcJUitm* z{Y_i8xaf$j?wxEYqq$}4*0a^`_xfASUATRFdx}w^z`7=~vzxA8@1Y}h`2F|G z_wKpX*4kRlofe|yYao%5ojtp?mDSA5te>TiiP4^4-tNiW^5`2k3pelB5t4nsBI;yH zS4z>}1e1VB6HY(va9hlN z{eI`wEZ4v(w|*>~I#pCmOzzS5-=Kv(Z}|^f%k{T=s5ohb?v1altu-+*NidMm5aD8J zcD(a0_T9a`?YBXrw*T&&PqvzyR$M&UMagkxh?-RIhW-2d&p)rvDfZqN0qVlvN>Q4q z5gQu|nxZeU+H_OqVn)l4>F zm2U5iJKoQB$;#cjQGNkJKt~G1dCP$WYwmPj@v*yyxo1O{*?S~&O0BsL+nK5hDvxkQ-Rcr{* z*pmHU{Qo~uP!&Z_UKK8o(@~1a#YnGU)pAvJj+lYsG z_Sv?>4-Y)tF!}7W6HkktO8Hzq@~|Lb`|XW4bMz~&JSxjj{2Vk_sx0f4;JyKv!k$C{?CK=pj&EgKYzFTcco@)+?m?zJAhQ#RlHWc<2lXMu&xQ$0uj zUwZHEOrD!wxqRC3`K@!eR{XiYpPov_-RpY zd4Sf0lPUFY)4g1vpzgDc9KYZ4V%@1;f1S0a?%A?sOM&bO#%teJGdyX0^sPGa`syDy ztZ&=f|9iCY`|F!Smhi=Bv*?U;i%q=br4H#q&z6{$9x~JEZQw z->&<6pWDh?Z@L+uFFP;%dH0{)Ul)B(c|ObT{jNP{s{UEsm)Ede{mKJ=9L)bK8Uaww z{zAR!-2KO!4?kS6dw2KtdsW@Lc3C+sH0YPJO)4yW89E(wwwV$b#3ylG=>UTh`7zGU z%46KN2T9dEW@=<;=a*l$ zYE@T=*2%^0{Xae&p8xTr`us^ADsiQiAUnYLlj?GWMR)g9P7cs$Is8yz{(0ek^@zAQ zwW(f#8X`C~=<4cj-m>M-s?&%Auqu^4hl+#F?gASRBR=_eGMU0SaC$d9Y~eymo4Q98 zLPKLTc%T76lSJ`1R9skt|`{Neuor1CqWn!^`|kJ2BpnXE89Ky*fBO4 zKJ||kb0T!yjApWUIZAE6?He4d?6-V!gpQkrh?-pg{zs>to<8>cvmnp4WC^yHC2hUE zyqj*mS*_;{D)bOIPMGV8$IQbI6$Dm1-hKbCS#Pf|sC4(Y`#FW3UydmtGSbuCo&Eo( z_T1uHf~9cXx$AeSz$}N*mAx7P%KAFoQsYp}M;@Tv3i@_zYJQvBnC;;&!7GP1LezW@ICbLGa{Z=Zaw3|eV2 zd-iM{Jv}2s!;@!cn^*q$Ab2rjijSK2!hnja(~-`t1gDq(Cq!0E6rbjyGGpe;|iwcfaMM`ix`{U0_tWG-jqrItZ9tVJAN%}N`b|hW5?L0rP^+M-3@6BI4tOU^(xCoj=wE2Km*iV zaakOA?Yg;#%A+NhXR5Dj&YT4rE`M`F@Lt_-rqa^Vh-pU;9b&qC+4#(PeSuCF4;9cg zqj&R;zby-nkN3~c*514KE@(*7qC(&>L;rDKwaKbt-4jo~v0t7!>&DwMP<^v@?ZQ>7 zxY`dlCOVkSR-Wh~(0lC0jRy}e&tgPMq=`UYOUb}wXQ)N>1HFE)u z3zshWgocWqOqsNDWu{Ok%gr1yjn;M>IseMLd*k@acFP_%IPtXTNK&EFM2^|BrK|gL zCD_V#&)v2yY}c+`5z*1gQoWPTK3lPN?b2Ph`VT);n5`T8`_Utzj}<(J4LI4FuUuze zv0}x6w9SDUA{|GQa`N&v?b);G`M+B^W-)r=?0h^A%63P_#;Q&}nY8`(iuL-otF4vi zOggFZu%M;4mv_;{7vBB1KmDu;2@Rc^U9vMqq?@&9XVCVgy}iDN9vXDIG}YMs4|=}g zcJ7ZF(JR-k1%-!q9}kWy*O=s&-f{F%PR<(P-eVqWll$LhFJHB)tFf_8=X6qXGPA*l zD%&ept~@BQ@=%!s8e53a3A?v^)22ynZ3>~Gp%r`Od@j40%~qZ1vuVo~9+AwPoI_8G zCiyL&Ib(*(M2|gJz9Ju-+R;%XsTuHjmLX5`t5;cVZETg#=XRHrlw7zV07@_=yG+hy zq&(5PzW)A$$^LeVQ@!>UL>zos5)d!{092%`UAy+rtMK_BF3?oYvZ~ss9W5u7&##vomTvgT87Ul1B^7ji-K`~G?-&y$JSQP7uP^Y}X_%c~uQkJaw~ z|Hm&OKmU%f+O0~-w#LMU#3|oOcgF1b{m%7X<#VO@pQoy?T)DDotLW24~r#Ka>tRn=^^r-e*nRaIL%zkK1#OYS~?ev`rDLszbZe6HUAefJ#8 z;n{n5uC zC!9?Kl}^+3;~%=)|1!Lov8ATI+}<9vBL2&A`As`_c3yw|^wH7oE!(#TMn)dI7B&Su zFq{8*b$soiz180tU1f zlUJUvtznpc+ST3NTHSx%lQ%cFXJln9+O(-DY2%4gr?ehF7LM8V#gJd#E+tQIXa4{A-FSB^<+Nl*b2j9HW**T~6^2-B{KNdYXq4?@m*2-0@o?Kg-xNO<7 zxDPx_*Xhgkw|8;L>rLn0eK+ji6%*e*as1-?aZjF{G?$dI@vxhpo|!q*M{V=ng-1>{ zGBdL@e=RB7R{ZkP`W@BZeU;*uw^I_jsWjJb z`nnSa20Y9QVh;OFpHgFY{_Sn$+u6@SfhPawpt*gstBxjy!ZRPh5PIde0_cW=ZnStCsT}$Jm$>1C!;38 zbLsy5<12&JJ(mWpS+i!6k6NMCToY5%!WS15`Q+`k$mU4u*5_@%{qgr|i^@+b@ArP! z%gtSDG1u?fwQCU(5iDI5d*f!!lFH1;Sg>i+q}SKi8{5=WpEtFb>-VS5URG9CqRmlj zs>)oy;=jLmv$9s5emEsU=hVf;?kiTVJovUun5%V1%}*`6`TetIWzCvByW;m-?i)E} z>tc2udhz1MzUQDl_zM>vEV=<2Zu$DPl!sYMTl?eBJ$pW%bIr=i%E-xiaBHiyi_%2# ze|kUuhHlxqweaDg%078}ztmLLj}>#KP8IDss+3p0a`O4z@Ao-}hl|VC{V+au{CH$^ zbp69(1`~7h;s*y9-tViP`0ZPnmbP|eY;0;?9-l_6cYM5lql4`BywVRJkMB=POq`fu zvTN_Vgv~cU{;auj_3FfvDW~SnP*YcDY+kz5b<#-}%O78+uCHkgUhY@);Q_2txd#s% z7hTj{wme-ze!ha#-9?)=bzQqAcCYHS$?-nfrHc-^YltLORsCA({_4%M&y|^(na}2y z>wP*VeY{cW`@6f(FD^E8S|}jbKfA3>;dsCN^8Ne!*RAt2Gc%hxYnIYPk01?^0;{<^ zd~%>&cc7wd=T1vEcXvr?Y4xdIx%GA@4mj99KR^HYrZsN^y)G6lIA-E2CDjzO=Z9Ht zZZ2qr?nU?M5|TSl|N3Rc*Ur5Ee*gXV#~&9yF0jxL^L|`;>7_{AdhyF!CW2QKKRGpZ zHK^45c3a${{GHDA_4gCX%K8c{d=>^Ac(@@rI{LJ9zRYv}e-A)gpTxw)ch*+7oIUGX zv{RdtGg>~&Rt!7Ju)`- z=+mOYA0Gsoo7q>aUaeam8W|rC+QV43d*!-yX$c7ppP!%KxOFRNWJ+1NoTL5llh3TP zX3esw`eNYTFLy9)^ThMfcXpTWmsxQ$ah6)6gY3(atlhg$**<$N{^9p(i^4}N&(F_) ze56xYgv+(YPG4DB`OePb=|_*M?upaS&0YKF-TQxhwY9Ysd+ox)y=9~1-aCZ^K3ohQ z=sbDrmek?H+au%R6eoLLzIE%=g$n}d=jT0KyWKB1@L_UZUSo*X*CXc~`n6-7ZS(hj z6?=YeE@)}3d%v8anc1-`S9r{#GefREc;NW!*DuhLRk{AjCsX1q0)nM9(=J@TeD>$( z=b&-znLceBHx|m-N~IPT3m-Ok@%C+M!h-pW-TNPX{&^y8^ODQc=iQjOa@n$3_wLD= znVHR)H7hOUiQdYPNdX!zCQ^r=f1YY>FKb;kB}VV~-QC|SzGU=xs5rU0vikb=e);}A zFh0Kj^2?f+hGF1FE}V!jE!) zEIh}qwrl(SeGbvl(z&_0KK|z?oPN4u)v70NZW{CO%BECRb;v*%&-T!jD?AfD zST38aT)Q?kBO_z?Y7fx3r${&Jv*+S7X3bKQSaaOK$7!K}eBF;?(2BL=^8dSj{i^Dd zv-PU4*Z=(a^POF$J_`dDELsF=HWWQMk+^Ersvq~h|5Kk|Q#AABNzY9;&y-!4W(U=O ziHVIMkHp98AMclc{_XAUGw04tJegAQ;elfP@9p*vX5atgyleOFodpkVuCI?jzDeg^ z_4`=RG~xXD&u?wbzO%Dq{=fA(Icp~SP2a`sy4dhjWw&wqxd$ayp!J>8 z_5OCfzMenfY}&?~K5{k{9F4JqCM@W0Yg6EBPmV7*{`ezkw)6YG@AlxG^oI^9 zb-GMS+IZmaZ~mEc<|xVa7e6{u2wK`v@qwY!rRnF-pXqkZ)^p3OtZ)B3X1>pFW5knR zUtia}udY9Q=8VqD5U#@v(@&p0U;mH$-_iX)p6~hlt@7M_dv^_yN6*ei>x<}Vmqr&A z{`p}D611B8aB;uh$Gz|W`X_CysJy#AW~WzvzJC3mp(XEiElFCPpiN z^XjTiH)ZVqJhU%(d+Y0`h)r`Z+-PKG2W?usd-rZmuItZ-{Qu`ooM?UK%$bD9pGEAm zK^5iA&FP?N?oO9KJFml&GlcP5GvKq7r&{ouHETdkvkB*)OUlZoW@mSYyxRQH?PJB9 zty@bky)>!YKmXJzuRD3>D??sgx2tz{WUb}9-qyE3$#f&L6cK)HEqI2iUZrQ#)aq~?H$(@I9+^|@^dbLQmYu8bww!{e@ zDl=!!Oe`vzbnjlAm9;e|3)7l)>n5H|;b3_MT6Vf`-@J<#LCGPU@Aj1|R|@|9vFww# zcPlR!hb)HjxqKvVJ7_&~dOG{jq=_d_X4cs0-^`iz^r@(r_;ij|CzfVM(E4)8ozCFR z6a_CXDERvJx-5?N@$)k@Gz@%~usJy^ORD|w!A@ay4whFEGByYP{IS`*xmbjYH7q=Q zM`iJhW_JEd*RP-6S)5+5JNC%q!e76tI*%sB?5k<*?DX8WZQH4X{HIR|^R@3Rey%4i zJ=>AeYtP8dUAXF0|Ml0Z6FuIn+!&!F)aeot7nhco*XN=X zXm{+=C8NuijX`^uoE9GV@`Z<+n>!~bXUcc~rI&Q{^o01_C8VT|JYRj_ZCP^UwR!2P zlTU8iDVda&HOsxZ>Lzl<&(dCGiz&Wd-48#a$(`eosCkLO*q+JXXfWGU%Phd z#EH8>EAnpMj5ISdJ9T}Mhf1g0Vjo{$V}lF+)z#Lzx@SYee|xG3rEK1rKJWPJuNN*| z+GP7|WmER`b;d?T?I%xae)?JS)8=7;#OY0-MG$`Z-P^k0Byj<|9SH)+}p8eb%|7ViZ!T`RKEnBxrYm&U$Jp>&}?IW5?7$jTo!BI(m8^ zKRi?pS}7vuugUUp>-yTM|NhmbrKj&Kduz3Lak9l+zFoU^S=9Ux0L>Y39W9br7@#r1 zYbmHznwZGA|Gs~4aPZIK?YEut^Y!cMO<%v3R+;SirtJ2G}D&t|wE}c2<5CtJ^<+bNc;7>()&R&~Wkf(&pmMd z)G4iHmNEO@%k`&Mn@sG?oMkarPe+?y!eTDpo;ZEJ_T=+>^;xU0MMOp(-L%H`x##lB zEl!D>ZtAo;H7;GM`sm|c*?_L2Ocyg=TCx=EoC7}O?8&^hk3L!m3k&P$>22I9dbhm( z@IwRhygN_kmg`MB?9VT6cW3+E$@99WO%uCyE2?m3Ois=kV|#nh>IaE6_qW#?%>|^Vk>yHbALqm^#`eZbF_Uvk_`ERxfU#j@i|5vWReb=t2Iey{?6Kp|e1#5S@JnDSN zwEnucf`Ed+3S}9-;J}B6-({TJ6j|=tz1y<*nXZ<$HfV9_ z`v1S`4XmxbEn=itO$`lC?yde_xEnMK^iKNkWWVK|E=ndQEFXXFk!Wj7l(3NDgN_Ae zW&QZJ{r?;fl}+o@4jXK^{g#J^Ct>r~`_2 z-{0L$Pf2Onx^?UFw)r)7@};F)L5nu-+;Pdxm9486ojO(YM~$77lvGA$=CWnWs(#O# z`l|kp--(x%i~GxVvDrSH{8s#_Y1JQ_n>lU0y=Rw~xt)6cImJlw(MOAwA+x%=xc0=| zH-3H7GCnj^H8eDI#n{lJk3X6u zczTnEns81|&ZDy3^Qv_rH5828G@r?I{lvKrM;AIffBE+9(xppHx8ELnS@NjFs?g%i zQ-=1`+G~xEJr{PJPgXZb+@d8Qqj@T5z|@|r!;BV&Cj=E zx!cXZUsor4-gaH0#M5fVgv~Q8=KA?uu1VUvGX`{q#I)(tXU?6gTYkS%Zui}`>#q;y zl}|V?-f{f#lFKib$7Iio(23*Ye|zQok6+^5N7dP8+WoGxEUVo=$$zhVt5ai+nY+@& z18J8rGTK5=0WHB(^yrB6pNsOf-Sg(yx!U zb+kLb{-Ed-CZ?qUN4b}1%}Y#7ocRBv{XTaMk*chKM0c&JNo8ey9x2?18MfaB9lA5q zXVTfU(_2GArtI3%;-n}hCg!O&xtdAVTGr{%Lyn0juIT(rwLi_&s+SSBUf5*zP|KxwaiLr`XZaVKWny zaKC*s(~H~Fa6kU&j~^`de_yui=;{jdwR@-pEeX=(YPB&nU0Uc7f93wIa;xkmmopb# zl$bgdG${Ip%jr$}=Bc~(L`-WEY}|P8%o(1C1wUp!-@c}% z(qzivk2jV7ZZT-gyfyFQ&c!=y*|Ptum+j`=e%sY9f8yD+8!s;F|eyZ%QsL4Mrx%@IHZ(G#;u{fDB>E+8zGxNhGB_#zfE(m}oUyF+; zr+nIc!96vVb-Dk1(0LJS*J^+~13Fcsx3~1sN0pT!lR~syfd5MUl!?hee{v#W5r7CKpipAkkiwrqDC{***9N$ z$+Elz@#*K}uR%z*+ zR+@dbuI$QNsa302XJ%z3BqvYq?e)F%@=ICzRMAN%RhDHw`dHzi7ObPc{tdT$g^k>+ zS6PO>lbsfN=uJPoN#{*jb!AT9*|gijRwth7Ogbr&x1D?b`R)22f~D?b6mDTDhL9wyDXVWHlE&W#F z`?132%Ju8TudZl<4xF7GolsFRBg5p>_I+P{`Q&U)TwLrfDJQ2k_uQ2CtB*fUOh{;O zSkU+Nt0`!^!_(8@-&ve}E-zWUn0f#G|4xf1|Jzt;BNr7Ny>j*Hqi@UJOK*MhwJI|+ zQ&C{W;k3;gH*P$5?b@~AzO*UxpMux>CKVS?uCePs{O~~9W=PjN!D@#jT zn^8mTa$}}$*G#5*ZQirTM@J0QDm{0OFD?CfL{!wh&HvARzIXMit3kwm z+kM%S12n!kEnZqC!o@1osj@JDWBz$}g9v|DSJs^|nL5|reGUx^3yO|*E-f|v`RDUt zM_4TZp*P*X9Nlpue{K-NF@@=yPox+v-C}PJTMO`{=-NJq_VdpdFJIoiZk=C_*==r_ z_ea;R&oB7@uQtcbdhXo0JiNR?;o;lUrDvaYOHWsSzyE*rpWF4n>pvW1zn_$_fPedK zUk#C@l9DL}7EZptyyE(C3pQ^~zMr9R*x<#RHxK;(J?Z~>u6*BFcKMpgzkdBHa!{Lm zG9gkdZoN2Yor<|w_r^VYd|X{w4<-n-AKs`QlDz%)$BI2HOpICC*&A=at!A>cn?L>6 zuc}8MEu^JqKXq`?nhLV5!p1K=Tpe`4rn&iB?l(XF*0Hhu`nlQuxA4BtbM2oW>3qz> z)cEPSuCDGJ`})3x3mvHqV?;rWGcZc05T)ZSGv#P49;`Q3g zKmPu!JwG9H)*QRqS?_C}->&(-`~Jf-Gq-=U&2Ds%jg6gaVs1XQ*_`2HMbE~KhS#oN zFW4C~$EI=;GdtV1`7_VI+kG}|@wRPk+wWPqmY0A3Ri~;8s5n> zkByD}^6gttZ0z22>D_nF9dNK`=a)P3{IlJ6YduJkfRX;o_)OPNT)S4IxVZSrb@mr8 zUp~BBem^lcw>QP8^K6>30LO)U_uSmwkI!3hlJE2;kLAWsiY#OF)YqO>o!%4?9sT&++}#y><9_}AUHN?O_LVC>Y}~fZPi^u+1D;o}UP(wvJ$Z9; zb57ngtA2^LK)GJw&K$w+qXiZ+pyuEA-<`)FuUWso`1`xqUAwGazIgFq@ArFm{wM@&B^whoBcFU0Gn>u3{yO;BZT)?{ zmtUq76?M%v&o?qR@4x)=!KbI%bF9m!H8wKdtNG0Gv0~2e@A5h245v2i*x}&oYs$;( z=r^xMc=~Bq6`_L$Jw@B6o8PnOyZ!c%Nqp@IrRk>ydyl<%{rc(O@AaS)I0V5v3PZxf zK||Hg&&@shw#?Yn^z4fl6028dKmDtFT1l>7nCF@>51VDh2L*=NvlqU-a7re8eca-u zOJ@qJ-&2qR9g%hP*fF-FNrk%)2g-sn=KlTjTU%Xa_}ncUo)uZHT(joMmzS4i>}*`L zrh*1zYQ9`Ne*5wCO)XA}a&|Q?)z#L%zP@|DUW;CGIWsIA)IyM8yK?>d<@@)KKRn#N zagEG${rJSRw7v+Pe><5?m%hKA+H_E%x3{pZB@&^ zfBW7B#J>BA}cC;*Q{G-WK`7t{a*F-g$o^R5EDU$k6cMv14p`+d*UF&d$v{?&dvvE-ooQKOrqm?6g+t<<6szVs;jZuD^a5 zR7#cKuWfH;=l}5FpmNeiiDj9W?%(%!cen2C<<-&G-@Idoht=F=5jsISV%ywbwHW$N zo;6FV>7c?)pSGz}L-X$JIJ&bqT~bEoNi+YxlV@jdpD|;`k-Y5}@83VZG5Pq1s=ZH& zETf{MC1h+o^rkBdaDbWr2H!A0rdks~bJ+}%f#3g`H#_xARJ=7EgU{feKT1Fcy-{o;khO3)z& zXZ7oU3U1xHb&kbHw&niwm1g=FnVR}4O;nia^YOt!XDR8~pd`0C{%`EBU%zI|nsvy; zch&0EmL(Y++i&|SO)ks&P_BFRDy5{P zXW~Tbo__xgJ9kFjv!8r&)4qL<&dw9IZ3{a$&$jvU%O~&d?gquJwe|7-x-a~H9?Jh? z%->hpPgw|%$yw1TK+x%|6M_-HXWI>Kx+dmyI;PPR8v>qS@&0M@#5qspMMtZP@jL^ z*wWH-V?;uBcK5%3by3l?Evvq8n9b(BY@#XhNk?D5`01(8b+NmToj4(zwDHEbf2`A{ zc`44$G4syO)^6wH-MDk-$!pidUcY_~x;W0ZyW5+Gm$&HCli7QIKI7Cmom6amWNq}} zHEY)fM@Ju?X}tX9TixE{Mm)_07H7Wi|Ih5^_6~HUHeb9*w`d#qJ&|lRzVd+TrV_ zI*ad&>AQN>^xggX15>rbGqbZF|M-}EXaE0(j~_V~UDTMaAFnw7{N$5MK&P>-UqAib zJ3XOJmWduM6DL|fDYC4wdq3}@>v}JvnV@O9hYxRybi0B!mSk5mZ@=A}D4`<637%N3 z`Ke`|e@{t<@BPjC+}x@B_J0H#l8^U&dNw;hVf$^5<-t$0<>dD9ip-ojbLsl^(|7H% zl9H0zc+>0q;(!yNvA&BJ4MD3OzkKoe`D5ptqu0Wwc&IpOh$xA53-Yypw^_~b`Sa(R zKZTiBu3fm`xNG-rNeKy!@-rKCx^LdJbaQtHO>J41zuUBR>(`*{X{VoZv^v%N|81X; znAkX3-9I4o>2pxh4bYh6wY0-UDQ4Ff?Kytx&(G~`J^r|IWii^~jGLeV>Q0w!9%|mQ zpj9J#_wM~sW9M@jRMwlC9{%xzC2xCoYb)!emm-rV2d5ZGb{!3}P?>(Z@cFsePd{xo zM)Y)baUFVS@Ulee^r_@X&$jmb%^NlxxN$?`VZoA`e{&tqEp}h*ve+imn@;C8Ok^wUDUZjq6ZOSf-tcTi}!{q~p%uhK*g zZtiZ-sPB&-ET>POuFmRkQPR=XHMY0+&&}m^@0WXcak0C~WY0$*Ew<2eNKy=_DT?9|Kd< zvs<&xXUv?bSuTF^`DYa&&SS@p{rq{p{@_AqXO+n-KRuu6)AsWxYmS-q%^c7T6;e`7 zJMPYVt8i6{U(QBl^2va}z+KloRGe(&^vl^!JuUL__y4?k{@*#f%inL@vu97fN}$!; zq|(yf*VpqCW~rS%eOhJmN!!~ZT&&lwUE5Lq-p+6N*6G-jCo1Ld?hbA&ymN5t5&Uwh>X-- z?`<_VEj6_@dOKH6URy+T^x~a6XWqIMm18D-6Li7|M{@Gxp1wXqLqkuci5l|zO(tob znrr=i!_Ax{k3oL^#+tS0qQ=dfX+COafBmw`&VGI6>eXEK$gY;diDF`6puv04Y~I?n z?|1T@J_VVv=h_(qDzYDaWbvPL{<){xWKd5x!^F!(YU$dwQyml)yC&)1j-Y zuXC`xvZ(yT;#=9^ZmBB^K*CqtA75J zkFEJGFYnp^Ts`Kxr(d0UTJ-xL!~fJo&}uE&-FMs0rX4r%IvuAYcH%iVXtYY=Mvhw9 z?&cgbZ_qfq(1x2iH{OTz~tqV{HHK z?*DE7@uHgt-(0Jc`~O}4FP*o2c4w#Ozr*!EPCqZ3Atd;74{R?*$MMH0nZg~>r$@%csfl&l7x#Yp`G zpbNER`1XJJD*$ajK&XjAtXWUCitx4XEX|(r?Hg#hBjhOFBneP^bK3Oq6DgOT6p8)Z zt3}}wKx0m-` z{r~JQ-^xBOum80iwB*&=`uLSACeqTeWwAOjQ2zr6gH`ZSms8 zpml#0Hvd+g7Ka20l>E7NkwfpEX=QFqYuBz#j1()|J-4-$xum@O;l~ONw&swqut{gr zPCV5C&y*(2QuABRJpFX*_16bqmL$zmOW5}=41vJan~ zoxNrI_Tn!u1b_YhotdA%c-gYHt}d=SdFDTB=9O4ETgas4=1%SG)C>y?^HiHG_5Dk$ zMv#Vx$I_r9k2#HI@=Tjrf7SKxwGOvr6(P@{mGYo@;#O|)7cXC?6?{<0+dex+&)q~S zxvHw|;ltZCUoN_Xw$}E##h1Q>_0u8r{!)<@6ZLx|bdG)bVzMw`hO~L!qpwwkR&#H> zEd#I5X0_XNQ)i}6TZ)mVhDcg=HuvdGaiu@^f8BIb=JjjoKhytz`VZPUed9(#Y3bY0 z>0hIl254;9x%1?Q51=ahM~$70ocXfM#h@KMkt#x*N)tg-Ia0xD5bL4jr%7N}_^5fS zO;(lZ+w&@D-;_^3ZBC|4ir(H9U#hu(Yu8bwlPO&(Mn_MbI<;EQ9lAmfLOt1vO{M{+T!SC#IP%s;-hRIq!1b=eyr_UGHtG zf$B${S@>gA&aHA1ePy>vt5{wng2^Rl*{$}RU!S{9XspsQu6I|}aDA}&y}1OBk$HJb z*=}BkwPE24pj&ldf%>F;MwH#j;`5+YkNWL=XFsjY|I3}0p5E!Uc*ZihdT8E+QI&Iq zSU<5JGyyd}UxEhTdyX%Dd^Yc9%=n zy?(T7e*N4;iHK=mv-hvN{noCquK!%A%<}*DE?@hR!88AS`|-yI4S2x8#=rl*|L(i@ z%=;20W-RMWohfnb^84RwvJ2OC$ACkdCo?iZXdP77XoG-JScK7f#I}K z=GI#xT&$aJ>g*bBkD2Em69so^tWa+bPVyzwBm? zQ2%jBxpM~A)~8>*;Fy+L_~b<5v14wTTW8h$`N%6KCiW})J`^))h?q9(!fHq58dgXPdefsGL*+kbUCufBSyG}cCre`7>X zXJ?_Bx;iIY^WQAJi5@dDOq%Y?C*e)OMUe97wB@1O*%Gm{B`@~`;T`gO8lsa zV`@B*w{=$Bg`);Mi!N#$J}k*6W8q*l^T_M3Z^C94b{`gQ1H&UR-XfB(rIM>tuS&YVC0`N6?| zSyl%V81~=y*APicO`X}=%DU^Wp0l&FkFPIiVcMgQ61Q(xO7*g3XTRPOrTgsJvl+9k z58b*YwR&~72p8+8pScUfS4c`qK0MS~{G-P1)KjkVcX!^hmoHn+Z)9rfsy6xI+U@ri zhOK`4Ztnbr3qj{#AAA4(>-9?+CLezPO)-+Z^wQ*E!HlAve{R=nzyc0RKj98u@XYd3 zj@iw7_l_Cx?7FMBdUdvmROznRCv0BN_e)~4})6d+T+siIy2yw7%Si@Dff4+;7qt?_zufJ~Gu;Ik9V{AYFl-Vx{ z*2poov+Fzm{P63q-?aCo7&-Is@;=K}z0~C7$#dt*${b%9w?24nm}TWB&=AAMjfLB`nXL@V7U4>> zX9->za`O51oA=&rZT(cW_sHXqb*on&J?a`3e*MJLPZoaS^Uu5QjY}^onzV6a;H8%$ zPm4N(RvtNUKw4UrI%13lXX9q5%+we`}w1|IAGPJHd8@IZaCq>@zcrMG1fQBi5h$<1M_ugV+GK6~k9iHDl- zZrw#bK9^H6GCER>9G3o1$|W^I+? z=H{MqTJ+XiGls2Ey+JF_{P8OQ#U){@16N-ykZDga z@X(s7B-j6N*SmLf#4^pQpU+)Kh7qf?R*FUbu&vFc0t2%xa&n6P z|6>AG4SEcB^Vm-tNy^I~Ht4y3t{Htt=YAp9ta-|B%Vzs6Z_b=0(bjnUv7!Wz5LYV) zTl133nV`{UH|Eou7&zJws|Xd^$nk%zn!9$b&e^o=`?b=&Y~0-34c^NyAKjL#G|_;W znOUOEacR)Pu+@pT%l=CpFD!_iK6NVl-)Y`+{n){m_#S=zb>q&Ru1kYnwr4WLTwe+r z^nLeWm)^pFjtCvE%{SlF|I1WZwlCl3dg;ru-BY|&4?Q&aSg}6$ivHt|6&@;&BBq`E z_RVan*VM4pclnQgIhGT!Hf&``)z!+XMwR9O4Id4WWdRxiYs0>;WnB=n4zzCet=WUR zh3|vm2?D~{C&P8c#$D&Mk^sk-)oWXv6m8`Amt`i-V*6Mj^J1%SRfz7<=bs;aXD^7I zUbfqH@x=-qkw+Hi_Hh~ZALC+c-m%Vk@4W?=U)q%2U94~_t}Ri3gT+(jk%gc5VFL+C z$(!YUnX{H$&eS=5==Ik}-+vpP^}nrJYGwAeYr8HexPJ8**o-ZEnrA z-)FCc3O?=jY@GG{#9}Sb%J$Tfl9m7sm)&=lUCanref4A2-W%KY>fcC=6q}ZM@vT*f zRqU?2?~Hp>CDh{9vmYpbrEnu1FUM-Wm z?;lo?|y7ivYW4OF_$T-W0m&vi>#>hlW22PoBZ(1%;1P=Mc6ZXoA6Gtew<_Jf-D1|ozoH&k?ufN)Dl&>|xJ!_OWkuNPEm68L z*O$hv4_+CPxph`fPR^$X2b*iN4bR5yIR5-|;OeU@uD+^%$89&iKf%C3LEy&Pu+J}N zOiLByYybH3&z0A`wXc7atpE1Ec>C?_=&L!~INGgq^726Q6Ef-bTjl1T2Tf34%(#$Y zQV=^mY_;$1yJ|9g!aQt-JjwAAngSdGeC;2rY`5HgtN&|j7fNTYJ0Z}Ih%?J#_8uKoJ`>Wsi_Yz2$bU z(nOAxAzdq1YIYrEl9rxbvYT(N-}*_XU%b$`m@!4!olm&?=!vw=7Jc3tB1%)e49(1% zQY&ASSl!sx``98UFHeYv&2uT+%vrOJnD}-bRoZs<;3l26wo59LJ(VVMl)t;PFhuKZ zbpG;WVV*XNx<4APUxThrzWmk-e7LQmP^Ze-wC1Z@M$;JcbNHOI`` zKq4h4N90ATzm1&!%aU0UI&RA^-wa>7^zzG~wbv>>BuqVdl9`7;{YBaCBT0c#YsEfR z%yCh2)DTgb{yJb~$jcI|zg(u#(a}d9Z>&q59iY+jct^`&MWdNK%Q6FW#4=4@ZMmH* zb6j!$eKirT;s*y9C!GX&D93EOblgV+n>mUyd>7wZRoKj{{ahM5z3ld}=bsHNEIJZr zxGWBoIj%UG2Dnf}lIXw;v2M)LMpG+&B;-$Lj zrp?Y6&^@Xl6o5Nz1{52k&+nx8D8c&bBQVlmu_w zI8s>U*zdK}X?Je&ti4$}hYug_>FX2VYtP&orK6*>Y0DNTyZQIUPi30SnsQpS#!h}? z#GH#41KZjzxhxipT5IO*eHt`EowV`B{&R&gIlFyVh8!|Ed$-H*Fb_ZfWWVKwvD4SC z)y=!JY8PG9m^^uMPY;hUS8ImZ>^uAG=eM@1 zmX(#gD6{9?IAd93Zh6Piq!c6Pp1!^Sy>JZ?E{i_r)mN7UXe7-7UEectqM)0*yT|f& z$gFSP{e5R&y^@;h#d^H(McM8xx6Qs*F*h7M$QZOz`IUTERVAfQzfWz_VQO^P?-jTG_TT0|@AY8;E+{DY@?+KBh-sm(ORT9ue|zd&HDTcFH1D0p8ESS=K8{@wSgKUH}Bk8 zay82~Y+>5_uWQ4)y_Pl|f4uN+-uLed#FyRt-@UhJ=b=xZjC$QBN9c4Nf4ne4=aWW5 z=z8-FZ*`VsLQZ=3=C$8)=dDps-`%Z_{}FY)RCB7=6tAWFC9&IY zFO6DT^#9*q{gS(jcZRJF6%(Jn@4{AwZFg6H+q8A-*E83zfJDm5&?v%zxtrgSO z=8uT1{BY*`+p^tLPN$xF`YHJOq_fZF{j5W4LVS8^wEqn!#1%;7AH6{I@fa7E6n-qj zUf({{+!Pk53~P0o`17arkH2=WzOvoT`+j=fBqZZNOq+I*6%+4EPxWf`TKXx~-x*#R zKp69ivwor-BcPNDQ;o^ZfL@hCjc4)Yv&K6gZi} zbiMWM+uT`a)wbpSzxF&yf^AyrM8D;oZptC6Lr*>Bx|;QMyPf#YyJ?YP2NN11bo_M0 zQmc16EDc(CHS6K7chhZckxFVoLBXHQDYcEoz3Dagf1Ig4CLFi_MWWrquNNP3wjVy} zp;9H(6L`E(sPFL~$A3z76%{j#)BQe!?zM^8Q83SGp~K>f3&K|az4rXk*Q!pJL!kSN z8tQ-N|L;jL^4xqgB{%o4x!txz35jEu<-&i)3iGvF)coMM|MQ&u$HVgfA-5Iki zMC-rfKbh{1Kl?W5bUP?CJhs?!J9pRJb^q>(Azb>C`5CAOvijP~l6U1+-@KLv#j($^ zZQotJ;raRbwfXN~Pxlhdnx||xzu!ga;K640jk&WUblR@J{u}fxX%?GYfBVFVpcccC zTeqwxdOW#aQ7?RYlZ2F1Qig`F(M%Qa1>GO2_DZyMzP7rt&3Ey|B_Uc9&p$tzw)xGQ zDH&$7_ix*IV*N1_U8RWzpi`>O%7U&M<6sf#f*IRjV z2z(+LRCa1lOEok#J$Z7n`p%-KszED7;?}q4m>qYQk6j$J@z!&bZQj8U9_K3Hq&itDfa_u9$u33ea#SbE7tDUqp>fni(j8FF`Ai>lr(4N%*g`9#cgZbn|oWN z+j`9!o#6H6zrKI1uwh}UTefVO$I_q>Em1Qwv&i`P_w$n_K+T%QMwO2ha{@FvuD@np za5pb_{q^6E&82#4f6Mo}fi5SX>B9s%Wo%EJ{NclrN)tKc`q_^co_JdH_xXY6pC|jM zc`grjSsb`0PJdf&@Y-u-(W!4igC}qO)g~)WmQeUq14}!oGf{$bYYO@=6(AP>eqb~1D*cs?R|Q_^qI!E z9nas~++6cmI{xvEjf+);IJ34&NlVX;+5OEhfA3eb@b&w;A6xkIa4or<30fRC|If+q z5>ir+uC2ZO=dk^sdeF^rCQ`gPyPIEIebFmrT)dxuTI$4;JcicRr@!z2zuvO`pAI{p z&xh&tKf^)I$hT$lt)6uX&tF%ck=sYu@-SpsxPt9bmL zGxb0C|EpVDTi5(}=>F&V|3CG?>(_sKFU|4eWQx(B+4Y}mXi!=K~-f7DO(QTzGK{J(ixdU}Uj@|Gyydo`ca|GY`Je}1f2dPz{`xw+P- zwb#cyJTVb8j96CY1={)eSpI(t=n&!m|Nc3geR)|OwAcCE&CTf*f4|QEH2>e_|A(*d z|5vIPU)Ol^W~9#PC2{#1cI`T4{XXVtulc={+S+EXrJr`5KQ%dG8tCZFOG~>!2g5u* z-Y?kewozwtQX%M8fxP?kKIEN@&i~e5w%gZ0;>m@D&M~{bFiqEwPs&(vecPIg8HI0encDsPVRibn_nt48!hcNv z|H=Qy>;3=gEB}5q-%!{tao>TR$Bg&o*F3X*%(*`Pi^2R4zg~Q3Is7n1 zMaY)(*owy%oy%E`=9QmJnRM}Dpx<08PbID%kyfrew_Hb(T^ycxOgWAFNpSJ&@5_Tq)X+p^hLubLL^lvx|ronmzI#0ie% zHH-&X&<{C{)wc00xS=P&1Ne_6KMqOUpe zi`>?|ijs#7HssFsTI#g;;t>;H&;@%}v;Lk^dt0{KKFvn%@r8wv`~H1h|M6D#dP9>b z%PwYo`0&tq(@mMT+s;0Cps;xH-mm6Tyise_wnoWbFXd!w=HXE(U){&EVe3}cwPDMH zRwhWW1qV;A`F*?o$j!~^A1ZAAT((jX(3vjL(T7cZA#<09OJlEHH-AxL_4mWMiOTL4 zWp6YtUuNe7jS42Jsi_sr(ew5`y=MLT;FTdBYLj{R`HMe15cyg)H*2dGc>eHDo&Dj% zhe4}jZ{C!&`}IPqySqClKmYrF|Mk}opPa1z^RfLux#@bbNfj2(J7dzawNGCQdvDow zJQ+0KW-;T?^Us@h?fT}uTJ3G>EVYLP9iXK=HJ@kSfAHui_m97JT}PE(zn=Z(jmXQg z-6=+r$BrMDkd%BGu37bEPNj{U-#nXx6P4R%&a>+{|2(;%U_yq8+t#Ru2bC}I5zyIjQ#^jd6iJNZ9fR?)C=L>VSN=QqqPV_J^H$Q%`Irz@5 z(%In4KHk{x=IZY_vA#Jql7)#;=J?D1)j#+@?%xkButC(4Qln)zS4R~;Igxm7 zo^AW z_8D{LEV;aK<6FMy=$psuW;o{(jSr9X+Wtd3bpX-`u$P@0UEQ%ch$$TcdiPJ{66t|GV5}aUh?p)sdaW z&o!oNr)-Wq^w8k)<;yj1v)4cT{BuYBf4lJY`%XW4q;!G%KM#+JW&J-Jbw3&Pi5;I$ zs(;qe(b=$TSJ&~!pwTh~8HbU;nU^Y@pAXxWFUo-nwvw!!GM`uv(h>htd`i_sJA zKMtDV)8A9jxO1oSlFOQ}zFOVQYtO!JmsVOTYG6_Rjt6w$%bA&%ue>hJyZfu9wYBwb z?Op4>lQ(W?OxN3c;MA$+yPw5AR_Fw+G!fUElb-j^;CP?x;)pv7gH{G;O`WeP@aSum zgoMO{J3BXn>YSt7a<5&xwxjHA^0w{UH|BzFyWN<)*CD`Y=7IO$i+_K!xtY_Kw)v-z zz45~W{XL&hi$<3X3_SuQF%?vVDCa0%rhgW<&diu(1-e=F>gBn#` zN0p|f7JhjlGkNmlIX0C|4$KWpM9om8>^`%?PP6ZQ4M8X_Ct znk9VrcKdz7yE~Tu-kkq8rL(gzuJS3Dd~Hc@(ngQncR^#2rlu#$_y3k{I;gNcul7*< zzo+#N|NUjn+A76Rw|{-$NSeH#%yB4(i@@3iXmcxSQ=30ZU zaqfHk;oa`{pu5R)Zok_ev8&`IE2!@N`1m*nTXV_o*s`*+h?tlq7c&YT9%8*$|DXBa zC;k82Tcbb|p7(0M+oq*0E59we_uRE<7w5mfa)oi<@4NYrcj>*~`(3X7v-$t2PoIiP z^|G}w*3~!6|MR4@?nCqY&!@EYKYu!{AE+Vn<3TfjVeIs`Z*%X}{niQ&W(Q5&e|jpe zA;JY(94P^fBaqB)8AkG=Z9h4pU3k*fU5kQ+}yym*9>@W+_?kVi+S;)An2@?%1y>>;M?rB_~5NuQvZMS|3C5ec6o-`Yza_%{`s%3(lx(t-+%P`{{QQsYB6f{r~9J)@&VJT~MW@t;)Ki__>_^-Y-h`e;m7R zS^SLWW=`MsdsVZOK5ni~+O%iSnc4aOdbZ!MYmd=u&%F(rOxVG~#OOEI>f};y{eab1 zf4=m;{~-VWxBWqD!!oy5pZ`|v_o9u;k3l z&fVqj3*X=SdpG{k_f?HwR^_sQ3&4*@Pxtip8d+90#H7F?KyLr=V?9P9EZC)1rooBM& za?ns~T>hJN{~3gZAA=SXt&81#?$jx+pEdj9YnO+u4%V9<858s9#Kglg76l30P5tLY zt##XdcUgc&!P{G=lTNa%-}j3Pv`GKy^!CY$C=NJ6_RjM`BfA!WK z>%12RJo)?G{=nmp0jsa>D1M%|zlqOb@x=p$GN+yz+0E}SvGUdV_T+%;=9@`{g%h7X zeOf1Q%z&q?tn5Tuafnu^(?WrxNfSYLtz>QeV?`2DurqT+*sd;dAmfI~Ao|B2_HdwRdtU3~wYJt8XV+28N> z3qrKsa_d|4H9xihO=W~=o$VA>_gEe*V`bvKGbXM2_hrNAwPxDd{E&qnpEvFdTWxCh zJR?-VIeI1IAR*7gMvUpRuW`^~pI zd*~38{lAZ(gF6`7+S+y$JylIhTgJ)4^!wPF5Uo4=Y|o$8-+!c$SvbYWdCi(5nP$<| z*WRnoKmYN=!^3wCJb!K6dHuDinfdk^bLWCKQ-d3x3-4|#jGZoRp2t-1>I$ct`tl#| z%J&~$=)AeaN)|Nf^7XYjXs>g*|M9{TX`7!ES^8W)vPsA1vWf^-q6Ax8q5v1`$Ev+E zmUU*%YB`+9)TquLd+V*)>ebmf`QpFc?_B4-{r1wZ)xn@SGbxd7>%Uws{U%bpr=A+U zEt~D2;9wzh>~ZC%cxh>A={?`?{r0@VhZ!!y$ zBWTUzap5OLm9M^9$@RNh&AnAVFL6drUf#tVGtPG9GiT0B@YwU+`~80pt*MV*T(r#E zDy6N>zoYp1yX4QW%XWj-h=KNiY}|P8zyXGK{_{dSZ1XG5Z{1q@?#|BN5w}-*s2qCp zMrUP6SJ>*4?ecX;{{H^{vTXP7bJ^Q(KlywcR9`%Nn20{|U*`>~kWFXnZn`P+wd$|t zeDj+(ZY&8~{pe!^Xcw>&vmLu5sWmj9mi#}+11ysNHiP4#MZP;fAjI%?o!BbWVsAG1Z@;lu6xE3Zpe z*np1zZ09>`WNCR4)Qi5lI=u4r+VWqFO5g>Vi>_uB{`|DGHhJ6a+^(ZaXVZjFZvssU zHXpqE)xGm*(u`#%mwHbJHA_LuK*|^-BqS83dI@&AeE3=8vN*74XU+f5ReR69dZm@W z?`JXSiu0Ah>YXl1clK0v|Nm+Ke_m_rQT2D7pmCacceT`nd;0ne3=AA>a1$x~& zT&fld8W^07yYS4j>Y-$MyuvHb=QiAzO&ZnvFK3vXc>cMcb%)%_>(VKkD`%Y*i(5ZE z#mLca{`>8_s)P<3d;l%rDm0p@IO(KH{YP>8l=SrHozJUm?z*ZV6WsRG;ft z{Oilh9Dl`mur$<9NCs@W@ZLD zyeT!cwQToZ{f@-jW>>R%*RIu>>o?s;?ehKWpqOgY&^et{P%z=K$?N>2*{O{8Fddb`GE@YU@|Fx#Xs?2^yN~Bn$14IAuN5!8^ zB-$ENXa0SEq*PIa%eIuKVd_-TtgUfR7Zm-R-X0(q`u8JvU@>Z~*w?DZ#orHa%k^3s zH+!AKf&SD;f1WEl*4?)M_NhY0X<@*A*2t|<)oZ%WD)&7OSZ({a7*qr!@XxQH<%wyt z*!)+kUE&s;v0Jf0{r{!?&m#{>?f)}pd8q%-H2yhQA;ns z6yRWykUJ;P>5{Sa)}8(P4;(voEcv_6RIilHJJs2`zpcAjI8Vmn=ZC}n6`#+}4_tfA z&in`?XhbqTe)+|WH`VtaZ}d=`?4tD0Hrw=LRqfxe@9r|Mzkd4R;r1UF`|D18DOn$T z_3v+PP(jk$%iDF7>1NKnuU~opJ<_i`p}jul=bg&G+h(M_|6P5nJXb<(+SixQ&qs+q zzi50X!{o-DJCFAN|Nj5r-QC~qzIVk{EI;|_)2GdT`|bN)*G=5{cK^5TIs41k+u48k zQfWUQIvfq9KFwa>aJE=WfWyFKilR(h#i{3YZSBS_hxca5KP{T+wm5w1;;6MjYr_g- zr$^|xX-!=e_dc}v=b8$ea~CcM#I2v6w%K!I#F1^^r^|e-+PfsE`AE`6^>ar}e3=>< zCVI34X!va2>9sZLHm|@JC&zvxDSs|e3AWv$PG{6>(%!Ij&ofyj-+%m2q0FqaV%u`x zuiJl3et*!)P5bs8Ja$ZN?X~CO9GPa(SF*O&{x-3&II+;V9n?i*u>X1TwU58Nid_Gv zwfVJ^&F@uAmNw6OH~adH8yfBW@=0lFpi}TcgK59M&IY%K!!k{zOr&^UeP#Pv)ti1o z>u*@g;l#G~_LFIwuVigKZrAT}2sF>n!=?e64pyCYR(*SW)spbNCzeI64bqybDb}r{ zr&sj)+S>Db&lSe(y32R<)#J|rf@x3c%r|dBUY5Mk z7pZtzl5^@Q*W0q{@;!`76JH2F&yEDGsCroNgR>lUEN$X_7S^nJ!4C^S2kaI7{$?{R z_3i2dNfK-qGe84jLY*vc%VyWux$nG_ow#{#Y(n*JhwZmj=lX$0Uv|dyrET8%Ual?C zz-G?F2M0B4?DVI4wf6S%O7;FdD7^mq=??{MC04Em5=-vpy_+(8A(G_4~c&U$5VPaBH^w`|S89;G-WRrahmz>i-`zmC2rbva^<6T=Aa!icQvX z6AOz2SFVW2^-s^*>a{Uq$aTxLVgL0>CsoAt;}VKKIqXlpq%7Cp{_r7B__{r3PMqMlnl<(4QPpkR zHd*u?e)URATU*;i3Uuz}j*^!`oi0iDZRNJA&6;g}=xF!zk3Va2a#Lr{-|xLN=%%^& z{`=>D{9w6!`Ld6n+zWPPxlXsmG5cydckVoF(RcXMC$7_{`CpXD-g;YR{{k|NSYabK z>#W-B*|S5mLifb!KYpCd!?#TCcDwsQ_W9@gGo}7cbFQmzSh!HJ>0pBWhCr>Uzh2jW zc)-}}HhJbuNxk@eE;e%a#bYY3%zJfdsrR23?)3-v|Nn0P=%~2<_uD@rrnwqOJUP_* zx#sKY^@+F5K7aoF=S}+l2lD?O)_**yzTL7Qp{e-!xq@$RUe;c9muoFNu;6MIXjjZF1H3CY@vn4i1*FubWf0`{=JNI?I+V1C6-7c)@Wr zY2xke&ofP4#b>P7nhIJu51RC~ka_gzD7TDNNyho|{q^tt=Gz^8^5luQ6xtGue?LPO zKC^tfYwt93bw3&Q7&*I`rE%+%CDfFaB|#1GJ9h+6JvB1Vzn7AnJh^Ib+iR;3Ezz(r zxew>8-y4~lf=;A5k+vUnfZoZcMG|d}Dnf}B6*Epx*PrOMbVqIVyxZIJ-*2ng^s&mm zc8k?qzNbZ<4cKjSi#CwhQRI#pBW z|8U7WKC#%?dw1^TyzMvdfiAham}UBVzWd_9uA@qG{n|53P8~U7q3$;abUWs*bHei* z?{7;ma8NTo629+~(#w)rHFeULO*)P)I&J^zX)DmfBy~pQqUfN{0muI zH|TVCc5*&@)_37$$&EXAmV~YTc+p+n&}7Q92M58k1r7=a)aTbY?Tk6N%^Ez}sb*RB zMq>HD`fu<4SoVEB_xX={|1NHgm{w^P4kneOvxQtwE_*ZWZQk~`dAILA-m0FaWTs}8 zrd;)JO6QILiOZ_%Ka{K~SKGEYI`@3fu}!x(>D*iHBBbKbw1bN^gt0U{H7xBNKkGDx zNs}~giMhP`%M~gTGS&atWdF2DK@WuQzu&kxD)#KD(%GN(^xw+w?d^Saq*Hj-tXs>Y z)-Ef4el9!j-MmdV*QjJ>W=6i>`+Z+qyZaQs?=P+I|J>Hr)@EXDy*g{F*6nRqZ{A!v zS>6A|iHV1cV#BxRpMUY`skZ;ssI1*?ix+O)YI=8PvHS0DzF8F&5$We@T>Bnp{#fu zP~-BJk|!q=d;8x9mX~i|7qfHG{XfrQ*REZAx-|MB^YOy34-Pif{repMV*S5Y>{VZ{ z2LHO)|1WCu=Hgwo)oyKxEB4#3J>1T}diiqqRwu{(KhN&oRrEA!_oEYKy>8Cm-`x#Y znt0*+{{Pcw&6;)P(W6OSM<=)AOv$-(}R)#4PvUzpBDU?eK(a*RQ8WPHSsx+fx6(?(6sO+8j#` z9(1hz`#L^2Kfiy{$(DWJwi>Sr(E>ScY0%EQawiI9&YriwaK8Q@_umig_ph%EUcX}f z`f!l@i=VHnxgVaN-@k16a#I@{mGhrVett@QT4ecipZ@7hCPqeU-q(FUZ)$71)NARg z`2WA+OaA`iHJ$CNtEcz!Fu(nkt=ZR09v$JFYxy}ec5hW|tCQnQAGgz|wP($*Ul60W zeC0|_UOv9ek`kAhXI5oxoz>Rfe&zoC_4n?{m6k4jQu!}E%lymnN4tDYXZ!B`cuaDx zMPZDwae2z^ZI@nd@tbF}?%X-Qsa~#gt;^%4>%|6bzPanZ9NRjtrITvzU;pxDO3nRn z8@cN@H$N|Veoi)aUyUm>GqY9Mn;Ctw)~i;o)ReU_SbO#AnpIb41+AR&?fv{~-ukj3 zOM@=n-Ce$Q$BrdIEAPy^e{XN~*T3)oxAMu^tZ+&U-j=&EZ1q-V)6=I)Hbs8v0}`*JPNiI(pPKc4v`m__~;>uXoMQTs!OR zGOwkR*2V8%SYox-{`cnmms_vLUHgBHZZaO@4(LIqdoWCdo|1S@2~6ouk}jHzj}1E zJ1Q=&@==V;@r$QUbxoTd{$$rWKA9WqA0O}6l*-+@s7S_LTrcLr)6?N!?p43fE-WlO z{=;qYx}7_#U%h*G?cd+auin28w~-6q8g;c-TK-gF%+)N@-rn9<()WL~Eph03ydq}Z zi)UwNSAD;G|JBmzd|5d;A(t=z-jaTPhRynwReKk1PCx&;e1CQP?Af#X=l*|sI$VWy zSABil<;%gZ-oCwh;J^Z%(|4kn{lnMCU4DDp_}AU{_pa}){+=2+?bz|-rEhK+&b26X z+8N^qlD~7uR+Ovt+rRLzurL1qzf61dafjbLo0Xsg%qA_F|NoD8mWkA^%FomOU9SHX zJ=f}M*RtiyOJ7`2^!4{&9k+gEfJR1LU0nX&ujhZgE8oBR@$r7!&$XZ0OMZXL{re{U zf9U#sRbIitzrP%lP8Z?2X~g5aG$;~e?bFlYB@Yj&u3i89R_>aZ>sL>CxyjdjXuMbd ze@$B(Tj`S%iM;&$TUYPckrzB&FE(sz)a8}I>oe}}^X%>E$*8GWba&gziz{A#dU|?S z!NW=GVs`EdefoTHL-^`N7bmr~w})y?-MVQ~;LbmR4~~7dnR;4u@7HVg*@=mcpnQMt zo}H}Kmq|LOLj+ig?k;mezem44x9O&7?2ZCQZEbM&QdU;pe_WIak>+f4nN0O3WNlrwbZP2b+iJhQ$1hGz z)%H>mii(QL3ZKl`nj^}^YFn{kfwZ~Zg*$g_*RES8kXw%zLhO__|mB{#NSh>u3M}m+9;KHrPZebnm?Yv)NaF zeikozeNFdj*4BQ@`uLqOaW`|q4j(@J_L_Cy;}ronuADg|dv~Ah;@#!xp({gfItSZ{jp)?XK^wm0dl{kiY&H+@svz5aLa-u+~=UiQl;8-H)_)2E)Bhv-e; zx@ps*j~@$5OH@R;TA%D%ckSA>ts6EhI6Zy+tAow#t3tG_&sFB;`ikqv1+5JWUK@7# z?(XtXjhYfY{?&4Kca=sTP4ZkC6j}YO*|gxNMXul9-@m>#dVA^HTcuZD ze=Rfc$h!NTo4b2g$;(sMwtcqVS0AD`J$P?i_~x5eUS);-{{B8(>#FAIO;-mJDk1;&@b|y6zbi~OBuca?P3*|IotH1p(&)5sg3jql zi4v2Te*Sa+TzP{nV|ReYl6&{!{ytIvKl|U`{r~O19Ar2DdOCjJ;`#r+>|YVK`lNx% z?Yy_2AJ#Q11PJ^nk_n&p{Ny&>$Z2~YSKt5sbk{reFo~OW|CVKLyZ+%~rM->0=i9f3 z9~S(szFulyR_mk~y-)r`X{=@6<$vc4cv_tnu1(%9*Wdp6n~ndv*IF`sr?=()+^2p$ zcUv@nDKYu9qZrMzTf?;d;gEV zxazm2e=pnbUHbor{Qvc`ufKhW<7A3Io0hFRuVlTA)$?ms>{%vrWQ&b>oVVrLm)Uz| ztv|i^dh+jo@_o6XvkUFN{rmr7UjEsorR(3-l+7!-pR+%0TI$M+88(H!^0J>_eB36l zE!Y40!9nI<2l@AbthN4i{{PSSU+n)szrCyS^EKi5;tW6bv^Xg)4O+SOKw@Oy<%&o5 zO<^(o;IyJxx26nVdiK^^`dM3F?bp%QpEv!9S61HM8qax45?&p*3)&i?0>pKRU-U;p#)mrY{l zHYbg>U7GW^%vWFg{+nfu)bT<)(+>|T|0U}loTtKZg>RLO_ky>X&;AtrKVM{T_qMe1 z*3Ozcf99q6E)AQImXhM~_wQdL9?jWjvl0_OZhSlG?_N|#MUS@wPU*?T<7hH-b|`FV!2)@42$BSOmGpVQ)qdT4(BbBaWe zh0MhpH&*b;+bx+lPcAnnC!@ADw$ygBsG@+}%-;CszcmJW(ja>bSr~Y2mzidA&V78gl(BFJ`=u|Nl|< z?mpYix0g5TZ*14y+t$YR^^~@{siEP*9JAHR?tL!~HnVF@_1cu{y?F8BS+izc+L-Jf zx-z8X;UQPsiVZ7@cKWnRAzbuBSt+3Tx!dB0CrUr>Dto)@)z@8JM;E!dyI;LJ_0oX@ z4ZC*j+EV|2i%oud>#P8cB@sHSvbL_edi9OoeAW*WfBt0ORr9k;#aUKX)~e{q1U0o| zPj~h491by>8K5F`@zbYGd3SfM-JE_tv$FD~@pfNkguOLpELk67KGw#2dwZWsF{=9i zce7R1mySL;+eIr^)=EiAUOeEi|Ix=CP75dS91d6-6uL0r!mC%SHr+G@)e=D~_uO9o z@lb|YZ0Ks!si#w~zAjz0%x0+qhX_lfUi`jG>(-QfdcrAdR}-PEtlV{U(WhEajRPto zYkqFhi`lVab@=*=uQ(CTv1120=TmKb-{TkW?nbU%yY}Usou9joCUv=Olj?0d^|Z@t=_E5Vv$E=6`aP?|*K1FXyc(VVH)f`f+u5|q0cX=6{NBga+H`KN z_4=7Jt-Y51+kNQa2A$JE0xU%`?pL#XXU~?tb>qf__rKR5b%$#BIIcX{DZuc$iFo1R3}SZ;bHi zbcvbj7250Od^YWKv1OT#*yV%G>`yHER8|J96hgND0e87s;gu9(#y903EfWD#W?;a;B;f=S{PtGog+IQ3obZ)T&^dx$I`0 zWZTBCnF{y$pZwleb3a^w#ottqgNgN^dcQG`^R(2z2ai|I`+nr7?VW4i|9(rCFYn{; z7vyVCy}ga+@PypkT2fLcY_+1aZ?R#_jDY+JZ)ogObQ@5=S-{ZFO@ zZOdI5w|;emPDWv2q_niO*7Vc;vidW9Ku!C-d-r-RwNmx$ZCf0)a>>%As;QeJO{My3 z?&tS&w>otle=H>_dF{Y~2BFRtm6L^X=Zbw#pFRz0tR%dA`}XRK7afZ(u8=u?@z$+b zY;0_x@-z2#mWh;>n0Hh}#e%P2=e+v5D`@2uHgiza>BXstK z8oGL^Ep<^^ckW!@+E@DfqT+2W z6%}>q(4j>!*QY+;s}tCcLbmmdsQiHPQa#Pb=q{nVE&G-FC?SV}%bhGqcyype(c5 zT}PAN=IWjfvAJhef7(ctt99#+9U7-MrQF^IYT;O`95xW)YZvKt+rH}RmL|Qn#1)Gd zJ2y8o-`bqszianyQLdZOp=nG@gXVU*DI58EuU@@cYx?Q8TfNmLOUcV$|L|eKuV24Z zBtO;que;6lZ%1D6L=Ts*Rkh#q_&`Y*ia%W8xU$D#*REYArd$1kgMX()PLs2VxbW=R zGMVFn;o;NU+S^Nhd`P_dy3|x^EtkHdfQD3W*8P38w|191F*E<&RsMe8wQJWxR$skx z`}X1zE6@;%=ERQGAzGQmn=j;TH!(C^H$Bd3MS;bduU|`bb#zXBuPrSp$vQtT5Y*1M znj5w=CU1Lqmy42=jLemnmzO`a;00BKnJYtk)h2di7ZnA?#%j7Q4!pao)SF-K&6;Jx z-L9gYE+YNMP3`OBH*enj$;SWh-@mhFNL+7Z{yo*JRBL*4_}Z|`mo9CR>TLs!2sE?v zXJlr2c6WQ9dj2^o{=8=vYgSuZTgj^{r@7h>Yl(U9s;yooduQgHJF7c8IeB?_UhMsT zFMMgx&8XcSzkXk~DtxpQzvD{CuZ{8`9FBZ`rhI z(bumr)=MUze{N!BwF=a%)VQ+lhmpaB_5c3Wu{N$hdersq?(+DfNrC(1HH0`%e`n_9 zJfGsi27HQs6A2C3e*w6wG>n9r2+_vZ44cizo>GiHnxzS&%ocF!g zmg_%ha3*%2Gc+NBr~{W30zNaXyqXmidp4_hv%>Y#ts6HgdK#S#o1rpSZ^gxoEzzoy zZ7T~b)|@-X_x0;n&H48)%(X7x79|zB?c1(}XU`rAlUQ=~p9aU043kBTjdi6ZB_}@5 z0;SQgu&^r`Cby2CN!y%dHv84Pceki56jek`{w z2sB`$H8n^_EO=>9=KSTEQmaC=X3d=&x-nwXV~a^kyp}F!bP}XQZlvIx+-gyneEPXt5b{?&YLGE*|sof6*xrtk6*ocb79rqd;k6|cJL`Z zZrH3a;d!N&2$z?Nkk`_v+{Jdixwl_6+_=34wie)(*U3#jPm3(4dZn65`B{rxwo3DT zS`@i7XtkBU_JkA-Ie!qEe|~k0-g^siPf(Q-A3y!I)ug8LCCH7I1MffyW}}Yx#f&Yn zxvFygr?=(KT&8;2WQx~Pm94kbY}ZT^;%gV_bP;7~lv%zoYVEfOjU(Ux%8Dk*$!Uvq zYjV_tt~NEDefIbEh&H(GO+AWo2bIz4hOJ)c10TiBv9oKWLotfW!af+q`^y z8bX~KV%?ygp}~@X6JZ)DtHV~?in&Zuxn3H&-u%g~{`LHzqz}bEl9U2IGgU^kKD)4R zx`~a=k_sEWq}@)johT;J@5JIn_r$(-V2sJZQ#@Dw5{g;^v656 znwE2SOSowt`Me?b(mdO0Iqsu2UpAcTO^v*LOLgn23l;Y!_lvV+vFAo5Dt+#}ZELkW z;6#|x#DMtt`Bl}`_GW(`RnF5nJuOpe63@)E)xA?6Z*j8eKd(OF{qIjU@%#Ldy8sOH z*jcjT1R#U;>V7hlQzJpO=A%cGvbNp|)7J1meAxNc_WYo;X`5}=-TVSdMv=>MZcod- z?Z4*FrxopvvpiH*Wtb=({_*8O*A=ck7JaLeHm)ezx#sHC(A=Dynm6Bm{%I@P`DfcT zzc#0Z6Py-aIKIwVLu68FWKrz*W30-8@FzoX7m2#HKcAG!y7#&Q+guacHvY>pwPyOf%lNM1+3Oa&H7azHNUz&eAGOexA=_9ZRBmsZ>a{dQA}f5) z$34rmrgm9bN3UI~H1R{&Qt9=!^RLaZ676*h)ti1b!$gFoan;pTUQ0Jg_3jD_GphRV z;9y^|;n}bq>N0$*gI2BxTA6oyy$D~sXt%2F?aDRfisKIP`Es*4A0zHb}tYhbIbURE0Q?CK*b$ zZB*_7IoU%+gzILJ%=680VbC-NqBd}A%>2k?z_VD^iL=$|){PriZr!S~_W5j(A=K@9 z_2$i;izj(44Z8X)&GGEA#aUZ(QehsdFx%2G%s(t^>+5EtE*OBUA1fX?(I(Q*390r zva+vUzYf+AnU+#>h6m(xC_W(0DVi0>;3$x@H6bgs|K-b<*Iv9>G3)HIS-$=c3nE^Y z%s+PQSjwzteFxfKTWL-8I+YSO#Ye65#Rc!Kqe-Wp78S|3pG*m=t*xCkW5$JJZgJY$ zl9lt;r)>^BnOad10qWTnB<#1cwg!!O{%(BqamTM;zf6pbR8DWYnq&5K7ihvpZE~QF zSa5v&dNs9Usk4@unVCJ=A!zlcJ1Ch)km%L_qSY8u2$LSw{v!N9erd~^Fviv zf4#=^(1HA%M>mtM{+Eh}4fGw0okYnO7Ww5DrUeMks={J7f0!U8nFck?F5z7VaY>(=QV zeQdaPt!b#%)T=pWw?(tU!o#O}m2T6%edKZB&$x3Zskn;iNcYsv7f-o8D& zEpgT2#m@WQuim{onwj}8SChl**OqJ7ty>kfR*R!4H#EJ=OKmc!gJB|d_0XZFO*c(N zxmZ&-zmAHI4h|2WzUX4aZlV6%=vOaa2BxQbP4tME=&?iA>(-q+S5BSka&rf@nYPyd zuUoq|HFWh=QNH%87cVZ%Fj=MWL4M^Hr2YxRz7=cEZ+LtB>*~rc#g8YR?@`}Ve{K5D zmhbKQ))PHS)F;cjt0v$0{#Wn*cl(V;&v&o7nZp{l=;Df9yLX$|*etns@7~%j6Dk-zWZENQ@m6y`?N3TgeFVM$XxmGVS-R+3#fUQ{=DS*%CI4>wp@2TRGxh1J(b$;x_~O7(%}A!g3BzIEe9M*rv9_;s&W1ZbQvIlH#Y zuvtn*Cd6uPm;g&uZ0yyqug#~NT(c@}eSYXPPtCc0+9F&lL$qF6`>%V^O^0^#hBNr~fq9xY-wo7x~^OZp>H71{2aWiMt?%m!WD`KXlo-CA^ z>Xiyg02(4scjf)Aa1>Y(qj&mq)Y_h+opUCgY*9H`8XKw=x^`XK)br18zn!)u%yh2T z(p68Niki;$73Dhm?PJq{@SQP%cKa9ls4aF|{4t_+*4xL@y=`7gC*|Cp_SoXBX+~>i z*}tUq5jrpb{r$b_>Z@08-b~27ee1@J3x^rPwnj~Uot8SQt?TF_D=VvQ&qP+lt=FD@ zT66ZkBO+Pj)(8yy8!1Zb=X(5P8fIPdw&7`@l;?(UwSdFSxXX{S@qp1u3q zCt3AmY4p{&u<-EtRX$szLbpa;J>~VR^FF)d0=LBrfBs~jHNXDDmET`&{EsFD9!>g~ zd|q{SZ1pSTv6qIssqenAfb56i27Mty<4$kx&SUtn<hwmeX4;x~WB-&J1+Y-%c>~y2xjWZs-(UVi@q^0R|Ls$n z8b0>0&x@}=lm0OC+u0w_tN*Ac{^>UUSGivt6bFC)@f@C#88t7F<*EXQ)Nb4P_r<}3 zjyA@J4N@Yf>71VQK6oM94;B9UlZPvgF`wUT|7`OIliz87KHL6_{C@Vw^XxyL>-51+ zEnD05tu0@|&6L9hJggWWC%?7ENN!pYJT=8TwJFYA#^kv!vEp9d`RCtVW9yJo2pz^Z z2}WD>c6B5d9hg2dQDPI1v*O_ixwAI;IzPLX-llT+##_lpm7*+@^iT3LRXQr1=YR5R z->k;<%jDmD`?Bu>$`I@a%WT$Eo|$ba(#sAlTe%@sGierAKOgEY`dY!r)kdxy3HMEm0$7me-AGN!!-0NZoeGIIA^> zNAlJ$_KLJN#hFGaUYm5i1y2VhuowMx=zG5UQqHP(rcW!@Ch*!hf#U*dP{aAoL=VNo zAy>AEY}Rqj^-jDTa>As%I;+A_a?+Noc4zMVx_N6=Ps+@o6`3p2Dzbj;o3(HM>f*mj zDC5Qlo*$TyBDpMS+tR)QkGTskP2Q5=<)pbtPUeb}OnPL`NayN`yS$eh(0$O1N7ZQ?&(>AfrfhFAjW1(C9?kxtY{YZYs3$B{%6iF+ z+>=HQqpThxHHa)qu@728JyPlu?Kk47CsUQnpmhAs`cmMDE zrWO{UB{cQ_zyI&gyYu70%+m~;+m`p3)xhf_0 z_V#Awq_2tv!tXBA>g|57^w#x^0OXNs(Pq zVu?~V;W?K>&TI-Xn{8D4*!$+UP6yt7B|4{rOr%2H-P=K{fs&8+WfT$Z1^=f8hV?Y<(gqSEnblHu1cC98MucAq;}w#((vq$O#WgLlS&#`-|xwd=3X zm*D7hIrQqqivWNB{-Bjp7F}Eco=ML#d8IY=)TyVRv_!a8g=oDkY)jOb>a}ve{o0Ec z1J|xulaZVpyfVaV?%cUvYL8c4&5DYSzW(?3XGo7(h7Z)hepxbKsIx_BVu#P=DUU5y zUClc6{4;1_gNDewN?)PQmS4ZBzTV1S{`J#obx?;YD*pV-dwVNEo%qwIwWH$WgV%;l ze_pvs$NOW2)7`tVQqs~__tj3GHD}J1h0gk#V%;YTWk4$`yuH2UqgSd;^jNWZbMVWO z`IAmAQJQ!nOnc?BS6_EsyIy?j#*KibK@*=>uAP)WvE$N9J6}J)Q>C$>xUBoPZ`IYP z>iyetwWWG9l9HTiYiqTpdPPN^%&4raJihhCt5-q({@a6API>fkhSS1pwGS-%R$a~d z{bOsC@4WAU_jh}!EDF(DI&os+(Z_|L;ai0dQ+?E~UcI_B!$j+{NvMX%l=IK0o?MeR zH$SGix%q8w&HeDbap89R7slkp?u@BB{;Q;p4y zRlU7Ul%-Lsx6RC)`_$8Gp&BArGfd{alr!)Rwzaj5KJQk(TPkq#&0TU+yac}zO>wCmZkcP~#()&6yLeI01g zp4ZY#$-iH`cyXbTnZ4xKm(^Bve`c_;{Sxh0Pl=os7H$k$+n9cS-u0`iua~i8>72fF zZ?EvLW9Ij+^wtPz{I<;d`MKB&t=!_JZ*Of)kDFd%wKhD?a;5yg55Hf% ze}A1_{>~Nsx{vq2{`>uYb=c~YrL(!1;@9%HDx92D}x4wVvVE_AO{nbY1-=Gz8pcS5-E>8R8K?`=n*Tr0|e!q9Qw7Fe?+3s&X-y3po zTUC5;_|x|xP5)ws;jHg(w~DC zmF@j}ZvB+AX`y=4Zzs=x`jfNO2^3K;FRy=f)_niEjT;S@25Cxy))p`C7T15hcKg4` z_}{mrf6d_rK}yoa{un zTEBcct^Tz)f6vPNy3f*H%P$wnxNB=`M@2`2=1lzk``z5!wv@lWx0Xwz(sZ`(&$4IzS(^4}tG9rF| z_kVq5Ww;1eD`@zpv60bq_Bt^!F%$Fh`0{smuHN0fx$9`st=;A8>pnE^zcSM}{^E}x zJ5D|A3Jc#H6?yVRX{_pGPteNe{QUKcE_R%Hda6&r-`{`xxj8>q1+AQtwRIL~S;5k! z{aRaREL$0&vn)K`cB%ZI2l`VyR7{PF-T(jS&wts>Z+GR_uU#_7CqA$IR2zTn+Wm~% z&PHvCCrV?ddZ}K`DxJ9uG%dd>DD&*n2%VFKbG{h{uk`cv_kS6E|JSm6HJ^RIeqSEC z`s$Vq8y4i0`Sv|twS9XyWaubxZCLQ)!0@F(p#m&#xzufKZC|~8d-2t)Ra#S5c`cp9 zbNIrMBRlqe-~0a6las=ma-;M2|Mdnf;LFZlyWr%73ll)6;JlQ+UlFXozb2^D#R)uL z(;Wqx6WAps@#6Z03llEAv`gKby((;VQEd36D;FnT`S4+b-#nXzCr|DLjXqdS;%aw3 z_1t{Rwr%U&7B77FF0Z$*@6T6hCVZws=>hFH9uB@Cc+P_^(`0Z`;l^G_hT3c0h_4Kr6#|B%-1X_U$@t&j%Cl$BzC1T~x3!bgcn?%A`(Y2ktllU26W-!dvHB2K0R%{&9D_xo!9)SbUSOKWP^uHDrpR#uBn zp4^*dHv80b^KJ9&{~O!epGffcY7c2 zmj_Le-oF0q+uPe)w3$CtS4JqFHsGn8r+&S3WeqDR$1pH3+-~s+7CaqvWmC%RXL8wd=vCYI!O?cDPTO=&YwcQBz4YP={cx?RO<0Ccgt%I#crE?%>-F-l$E4Fu?CjRf zoN2wQ^7Aq=F|nt=_gPt6gBAk&`n|il(3w3{M{LWMElYm>WY03oHu4QVeE2Y^J^?NF zy>{)Isk!;`+uQTAlaqt@$!BC`dA0NJUvhc5|H`P`ty4}u{q%Oik+Q{$l{e`GfBwup zYxeBRZ{Ex~^>mZb%+Sht%QtQeoNH4VbN}DB`I-|uvQtxC&GYYFdGu(~tFOC2%kJ~< zF99tvU$;)Lv~=l~osp0!@5)sVLM&u19dOvsCu_ALJg(C9`T6(@@9WCLKsy3fU4OkP zHySi+rzRZzw&>8LsNA+FQ5&AGUrOGu&`OqQ1D&b^n%B15zi{c&r)5h8FPmJwKKTT|@76zUboKuI_0H{am%e=Y64tR|joaddIom{c?WnJh zd%x$ic4-$Rnm6y#hYuU_?#>Ea7!a~G zsy64Z(!>>@Rd?rP{WovkT=gNr(NO?2Rh+-?r@B?ag9e|=lTv4P9e@1mseb*^nKLD) zo=yeTD;txKmpnTY{`7a_r=MZ7H=I3t_IRJMg#{?MB6r64eSdcsRHa|ZFqz_U1hiyZ z*1paMv>toi>s2>%vMMVhw?@6)_}6BY=#I`82O61wy(|A8vv_gxs<73dbzx%S(?fK` zrg*5B*xIgL?B4(CYIuBTdiwHrcXuzJIn&xdmiKw)y#0NBeQTfR2etFd2Q9uBpf~;c znKNgE59`kV_J@a$@8y$|lYd>d|GR9_#T8=Wphf_%uD<^2iy5FT7>gG#wyONpa_rc# zZCZM^R}o919(+&nFzW4JAH!(F&*N;ge@VGv<1BHH@bKxN`Mi&hk6*oecQLoP-mACU z|4#=k2Xi|-QA9(8>*b-=ZmZ&F+P}8{|8_qkC&!1KPbR=@_cq-*0-z3fH%+{x;{_oS$9a-bh~l@L@uqjO8Nl>FYG+pSL$#p0shv?d|Db`2T(9TiulDzo-}}zZwO+nC{rsuYyJg)=ug6)h zogQzu@^CvpsAc`xCO0oHD=*J)Yn1PDzqyxMxy8R;p8s#J*3_`uYl@$r%f7$QbFNLL z)B8Q2`@+Jb>pNNwC;IxlxfuWN>30#9#&h%S*Z==^|DQdmO&qmW?QlkGNZrrV~_&BLaAwX~XWl+`<7On>MJFmyvudT6L4{EDiSUCL_W9RpK z)$#fJ|L&i1GUb+NkA@#PrJ(A&bn9mJp66-%UfGtceyBesZ9puhqJS{fB8KBzteR6 z_^YQ*b=jQ1eCN)bXV2w9xnS7iXEt70*Q>S`RJEx)yEPyZ7+3XKC6!iMP#e-?p~kTeCjj$lCh!r%$IQesgbi zYP@^b7JM4a{FF_yzL)*xTz>aX@ABodhTpz_k4|2(s_Xpo#ryZq|M#yhXr;;K&6{m1 zK78>HJPoe9QgjDv59vK0CuPdv(|}$^ZA8^gYWFOKY8nx)XS1xTmMe=nwqrn#HUYO z>F0VbT)*yKUM@b-V}{q#rgg7VHb;U^nK6-i`0ML-%kp{XqB?~#pkrg--YyRao$9sp(49LqtDF3a=U>~9J3DD($LXhw)~@Xh&?tHI z@A6l+*|Ve9tk2)DeS7_rjc?z|8qMTc?l*T)kmmMPi$%Ct8BRSlYIJD$`&VRPKu3yE z=k2$4g|jIwxE6kH2*yPoK=IkB+{5=KT5KwPF1&tbDg0my~P~=3xWf zGS%Jft=^{WwbUrSrf~Mdhl&4A+W++a_4_yI%(iQ5qt9=Nw~^y#W@g?IqbDXN=DG9| z`_v>AH+Ox|(OSyKjvtSVih9If|Dz#fRpi7ad-u-Wey^(e;K9okHuKh>S$X8l8J+xn zKiO()_o_^OyJl_c%Inf4<>i-e-)_GBR&B1|#*G^rufJZpckkO9cUE2H+P%A)hnH8V z`)EdX_F)s>lDh{AWtJ_IV=yo?^Q!;PQ2+P(|0xe2D*9Yb$$G^&d2;ZYHEZM~FK^5J z_Wir((x4NY?o`Rv+TC|NEzddseD|`<6PtQ;PXG8@XESedvtDo8xnsxJUY7j2kv!F_ zwPs&K>dYyppC;$dkU5^&8pq?TF7RRTD)s-jZr;BA_}$&#DMmMw9W{>^-ne(~;kC85 zdwOG4WcZA2Y&_H^S7t3Z_WG-Vjg3osdV2PK5iZt8A1&0k-qRMFeRU!myT5R++kA^} zi|2j)p*$~U8%S+m55x4$(#ObxmW$jOGAu1ow=>d(eyznNQ5;s)X?|yOsAb_NvXb1#nWlo&93LGZ|#+1 zJFM7th_Ocf&h*o!C1$5XU#;X}bwB>|)L*0HjM9Czf-wSh9WD!=v4ZxvyDffEWqcrD z-&qq>ak5@kGUE-kNP+l&+cdu!m7Hu`pM^?$9ockS6|f->lWc+-4mz3Fo46;%JRr( zjnGi=0W*dO+wW?xoTw8nli}&xJg9>?2xo`h>j@x2JlW5kLRiKmDRBEqG3+UWYaSWrtEXJaQ@`^^;fe} zj51617Cw4>HEU`})_k|cjs_A6QoVBAR&!lKLl<4t0H5D-cK@I0|C`G1*FI11bAP^C zL+kI~+9Qt*WAwylEnBF;wJq2FtMlROmjAN0N<~LUPw`5;{3w{!+S>ZQ$_p)J z#Lrec{Uda0%kkdmi9cV}-U_d>+Ii!q-+{+&Q~U4xU(KBz{8TH?H+6f+!YsMH^FvK| zx^_9!tF<+aN{iTj#XI|d*gIYFN(`gkT@pnETGQHBI3Gm_3Ft74_=PhQP7yO zX>NhVrBy+Xi+1v@x|Z=`S6}XJe~u)R>xsAdjyH3kUC_O3WyCbl`RZ}&?aO!$8?3l4 zedTp&Na)eGWww{?E}HPJzutWQ`QiYL30_M7D=W*=%qVA9bK;20X4A`y zl_nbG=H~YF_sjp>nHqT|%hczx%OsT;J@Hkm6g-9Uw(q`r%_H#1H9PCfdg-=<=g#qg z4m)1EcI&s7i~Faap4$A)MeJmXP(#(;bC)iC>Qv-%4eU7nn1`3Q=-C;Lzkh34njLNA z&QHAl;q+IVpDs6ePTAN7F!A+0wkh1H-n!oB@{+LC_GT|qXR%$)GM=GwnoF*qJv4Nl z(ekt%D|Wt>nxyjZV+98bQ-7@8RIjb8M3!EjslIQ&{MXVx`@;sH%XX!Dl|TPpr7mk< zxBvYD)|tLLV=CW#OpX+rdg`&fIE!P&#mre{7t%KMU%VIy>hzv^I*H>7YE|)JuZaMc zkp$<&55EgmzS?M=FK&HX(&vanpr2kMb4#sV&Sj<8m2&)7URYI6)iPem^Run*;}re7 z+=se-Sh8A_O?)43y&m^?@ArF8r-rYabo%MdTep%bD_j5mtu>knI(X~cIX=)X9H;2p zAI;qAt>Wz&Ul#5BP__5Ork-`Lr<_jpxqRgD#|sxHst9mg$atZnr?+wQ=HfR$Tb&yJ z{uOCUl$n24`QgOlp$0pvzst$jeldJn^mOG~)u5GEHcs=rux8WcH5(q~|Mi)aVzO4U zr*evlXU42!Cr%jre|P^c^KyUl+o8I8dK>rc14Z1j)oT8Xlp>`7oaH07+L;o*t-|hT-_S=_Ce{Iz_np=x0_zpZ#f*6bo#Wm zx}6*c%ZoQ}Y%+?!JpKJV)93P{2%R9k>6|Q0IXO8-*4EvrH*Iv6gz8M2XW8?qw9mf( zxN~~CdSiq9=}j6UT_LNt3cJ}xZa(D88GAi2bZUf-mx6#zVPt4%r;E~$zyE~6`9sii zQp^4K=>;E5K<%-bpQ`8P*?NbEf3{hq;lIA%+j51|MqP_Oh}VCV3tt~s`KD-Fu6#(| z>WL>)Zrr<yz4{N>eQR@AXDy$o!uhuFx~FJ+!)@O)kB_ZiDdMkT^I;8J6XUt#N=FVU?veZc zk#*KuqXQ;}Jk43L>3c)c76++@u3r4V^N>wk=tBJm=?Zd7_2rlqqE~L(F+FK>jAC%U z&Gu5wNiO*Z{AVBc4Y=&kG%3+&vUb2fvEN}o7psY{vINhi>({*vB%VAxoN4#($7E%5M?UQZGnRGUfA4;| zZ~Mwhv0Zr+PCwO{>SfXA?C)=WGUc};xAbd|)iS=9J=7*|*}nbp=btB@e_nD~Qs(&& z{R`{aYCxBR>Buav6!s1cRq;HuNoUnnsb!f8r@0O$2+Tk4ZB|`kv;B7O`RDF|Pm~{L z&+;;mIP$b8F;eVlQD@ZJzTDd>ngV(Ij4_GJKgJ%An_4Q3j8!J+FA9`rewW#iKz?IKpoh}x&zeM=u z?f!UquX91Qpa0?a-z%@Kdh_;e;L4EsR_hWa-n@M~@$}Qo{QTqFX0NZ+n{K_0 zFEqDpcZ{C+*DBuI+xqsTZPIyMwtJ$=%B%BU7xb*J{=>^R@7VLt!FuNX2fdblI`ey# zWnN#=&L$O8=aAf7&X7>siv9JL>DzC+=I8Twy8M}WT%5)6$382bH7<(-fBmYe{CYKe z&$nBvWA;`(^$^_>p>u3y@N&7zWyKfL*8kkO@ZyC`6R9g%Ta)*P@9#Zad^UT1?Bn}? zX78_mQ`BCpG2J^SFK^?94IUa-{^V@ix%2ClZS(F=UANBf-%b19*;Uomg*#*3ynUNE zYZ+Rdai}A3-VB@l7mkNN%`#QtTXnaFTh>m0efhS__wRbDNLcJ^c-)ZNoOoe{ zxklJ3n-fneCY;?oM|;Ams~RuP+m}2F6Zyql7x(tvgI6yZ8Nx0{F4^VhX+E!dL7IWc zgBTsBL+_m@sg!AjnrVd!uJ|b@02*Y{nwpmNiZMAkS!MFuJ-^>s|9Qy&UVi67eNUlA zhX(C%z0dsrKkVPQZQHTA*57|TXtuwUVG@;_;&XXP(8>j`e@!fvVUymqd-uziFEd$v z_pM7)Sa9InIlWe=#)%UXr%j*U>832gN zg;iC%3U}T)@oBnl`<*=gw{PE8?2VJ+o%{E%NUIa$?QJ*j+)1geog20G+=B-S>-Sal zZr%Ddtl~w1z1O58TeGiMe7&0OG3m(J+3_F#|E=GDbX%^7>%q5W#a~~Ef|4-L;e@iX zY5n&595+Th>NSshu5SO+5Ok?PeSLrR_jkb>BK>!9RuxaDPTyJlTrWHOb;##E2%S22uI#C&MX&e%(m6g~=X8tH*$cO(YENhEJbKjCXeQ6ss=pE3 z>yKxeNG%D<3|cAD>(*TS{M@E3Th8=KoAdSE*7x7n=rBP$e4T-X#h0z|!j1wh@1>`o z23_T__xnB2ZOJJWpc}ah*DQVT^>zNm+qW9zDy z@NF}@{Pw9IOni?%&)&Ais`?wx{JLL~|6khwP2Y6$&BXI)&D%c(XWqR@zWQ&=b@tP3 z2b1*tA8LQ^ns4oywQYIP&I=(^JIM`T;TvM~}K{ea9o_62;yM9~f?)j}s-aKth zJ?ghPT$B~OcNDYUd42o-*DEdQ3ZPwgIcA_CgU_G2=h)Zxg~#t@vpDj&@Y7G5zQ+$f zR#=q264Bp#Wlhzly?cE(MjY9s!*h5+h}OiDDWH(8sOZoRU$l>3HAxX8!wWX~fOsva-H+@AB@|d@gl!cmH{N|6fgQZS54JNS?zAB3u`*Uq603 zf4{4VRAOGQ z+|#T!K<(GGg*YHc&bV87+6&`1}|s(P;o~^i1XO7V-b;&iUJ%3 zZ*OV;sIe<6pS^H_+v3K@79!oM*REaLS^1fJ*Y4drik=!BO_~Tfwzu!`gRfPc#~;V+ z|0kxbtelgVw=gs6b&5duQ3=_-2X5SORhnpEo_{YXDXFW(>g?fm{!SOAn4Lv4|NXPO zSMj*Cto-}MQs&s}CsT~r9lNVue@rm&(3^hP#JB07!rr~MJ$-$XeAN8ceEA$37&;Yn zq;r7Akzc>8mSsLF+WF$mn+I3Jr z_ITscrAw97pSviXJ8*#E`ntVN3IYkKsja)q-*4Qofy3taM{|pcS>Dt21bdI|D1R?E zzrJpAja`3gg=a_ffTE$ zsolJBqvBn$Aj;_F!}IskHn!wRsO*hnn$`7NTFjwh&RVbDwO&*AtP3`t|9?7f_f)N; zUQ;(ECOux@A;jpb{Gv?oOC{&;JwNX1F>UfWy6xm^tL&v*KkZ9*Jok5TI<$UO%bmPE zar~219+qAYd}80fi2cxHP~-QB!Ii@I_iPtk)L6f#Xlj7QkrO94RDI@mBGwkzVmO`x9?o~ z{Muu0-st@Pz4DqwTjHdXDTcn2=grGo7q_=5H~P+g`-9)U@okNo>!aptG;_)J?cjUQ zK?nVciHU(m9;SxJ9ee&+$#a6=@<5T+bLafr+}twq^N$zK5)HXm_gnYTM~TLU`Jk~Z zucbvF-hM55ef#yUb(^SA0Dng zEfo|rMl*d@Tltoki`)M^*)J(4wzIpd9aP`#}uU>&h*D7q>RD_OgdSl-m zlD;!WX{s0KiW)a}_nEV*K@3$&-Wc>?&;!T6y!C*Cdr!U#*@N zwPtN~GP-&0#}A8TnIAtMmp3;3+VJ*muAEK9iIv@FePD1LtK()H`7j~o#JU&VUXA=GGQ zPU~J!hs|ZyvaRPtudm;Kw!i+*;xN};$2nV_TAcj(q@>y$7Y00N=Kt4`DW#*Un=ElN zUq-ENPSVCBmo6FY|MxXNA|k?Nl1h!8{M@;+F}uHUEnd91#mTY8PQJFb7Bv2wviW9m zqm*l40B9sFT9lnp=J>(F9G}aFKxb>)eA<)d?ZfI@yin$gPhMMyuZpLgL@?u$=8ZBw zmyekE=KQ{C?<{lfEO=n`;2a;G!!~Bi4?ne)JAZInL$3LC?Q@yPJ<3D;vAgaaD3PgR zW}IZFpSbAvfjn6Z6;6A|-Rz$4O36<#e4Ckjfc3X_ja{pC#Y6#|gLit{wbeAKd=>;*(}S zN%w2a+me%S@h!{U{!D{$)}jaHnJZTD^s{7$cph4v`6Kph6F2YaLsd_1ZPxKzF{$N~ z`aQjeFF}KV$r5a!8g^&QytQk6tE#JCzId@<_wLsZ*gAYyhNPvXP5b>_ede4wS)qE3 z&v>VNzyH6%Y2ktB{JowgQcssoKX$RS*=woO!hi>t{qq;C(ptRuxu=TI{j7aQlR9V4 zwEXwD{?C31dHKhOT0g5ymi_YOi-M=ow%ov#AwM59+Z!1g9$Xu}{pQV^mPT$XL!KP# z^}lG+n_zHaXL0&Pldlh=pOs4WTAe({$eXI^>Dn|E)|(e(MXbxYRrJJ`*C zesAyZhQqI3NgY1yeB^QAq?3JNvvqE7n=xZXN?zVPP^)O_)KC%Eh5Pr<-?-6GFLsxU z+T_Hfq)AVoPE~%`H+QaV-OtnU4?q8`c)4`?kJbBs2{P>ec^1@)oiTIfQ~N*d|C?8b zuQ#xT2t` zb7gyaV~;%kD0AmX^UIf#&z?Q|`Q`HY8S~>aSZDC%uhP}g`OqEzXVS-yhP!rIb-LVB zoqvAg-o4INb3tcD{{I#KKmXcwbBpS4JagyHjoDEk=NdR6mGknJ8+Y!cR8_Tgc5=$u z%~6|we(SUFl_59o-d!A`wRn}(>(|mE-R{i>{&xRnO--HkezoPh`}@y7d7^SN=i3qK z&htuBy^7!8W101BzNEbT<2N@q^YG0}EBIhCEj3U>>XgXcUf-|M1&bEO;tMgG+Ncf7js!^WL_JYS4W zOitX~obIV2#L~=|0^0XA>0}D%UwdOBgzh-{$xIf>( z*w|S|?D3bE!94u@3s*_)x~DMvtf8st(JxX%8GmI~^wF23k+z4X`b-x=B2&eKx8mKy#3{rk?|YW~~X zG(|ogd0g0aRA{P~@zRXjW=SO1@R+46Kvkxw)FHQBFanB2I3|M}bT;>{Zs8NP*?w+>nbpE-Nh(0>1cPoF+*G+cF6%Gh}Gj=Om>$JIo*CZA2yn65oz znd8=|B4=A#RQ!E~nLb8VQHP&@R`fjkO250?+os0i?5R_)HZn|pY>|_b^Wop` z`Xv!Mmu}zgzWsJtfX0@ns8l1#uA@x5)@|Ot{rHI!1`|C*=AUP`c=Guym;L0)!8-c- zo40SjpQT-9vH$7QRJ*@ljF}kgU;q8QLS43g(v_P1`T6Gk`+t8|=j+?Af8RR(`;V1j zpu3kozFhA1`iG~=qaQVAwR~>hwqE-^AUyo~L902+w^ccsKGZJHlxlRCkYePiG|?gS zYFO#CeCKJYck;|_ZSS@mRtyUZ(-h(I@$*Z(eXMv!W$@?Cdvh-fXjDx0~srH1R>Nd7QEcSD@bXPPfHtYk#dsu&a;|g{=iJIB~$iK7Zd& zHD%?)4TrycF^P`8ePR*MDwG-E1t*PUDgA zoBsWP$qc zE!!6S*KU-W`!=aF<3RuIr5h)l-WaU8tb{y-ZgA`qILUi)BmZdhI;ZyEe^{lT(7@m!)uTf#J*q1F41DGAzE; zDgl=j!nGcMtopdz=)_B%pKWz1@m%NoIj-zku<51_XmERX`TK<$Mw70r>3U>wcJ|9@ zS9Lc2zxv^DqV|%dOH;31H|JYbE>pWM(Ij8#?tc45ozoV5mwzqWkQ%vW&6-U+cb?3z z|8324_<%u=`n?0@zLRV0&d;?jpE+ZO%e1G-uea!Y-nfwIuz|k}vD==1-}2LshgLqHf7IDb@mk8i@8sVHC(0U|bFJ52<8HqirgJ(eH@A1~THU(u zyY08g+248IpSL-4)}QbH|8duSo_#;zwqJJjLj#Mg-d2Cv|NS`5&!1=K+5g6$`>?@} zo9FA!>DPVq&bfWaVBeo@RSN?WvR*M37k>s%sHlep&YU+dF)^`m{@*w0630&FN^H8R z1G*oV|68o_n$H%~QVs3zuetI0bo{?p6U#);hKcsSTfP3)egBCkQ+AfW|K}*Gz!`hJ zzomxfa1QH?y5G0&D|=p=I8CDc_rXnTlHcEq(R1Gzkx*8~=Q%0r_yx~-b$=q&+xqvN z{J8kFRdn?2H}BqkJgIKpx85_ ztnJwOaC5op^&eFVx6kJOsPVpcPSOV5zEvySqwP*|ZCZ8d;nxWq?G=`S&+X6ZecIid zyYF3SR%e>_itSdD{yZs>k5cJ*b*V3R`m6Ac-+N;Gk9e{$?~rNl^y#~s_4Z>k_kGX; zqXjv)?S$%nTFESz{8DjCfAaC(+8=k+S=INwx_##Rho8IFEq?uL<8hheiu2EZesIva z?*H%ambJe`*6;r()$2C5`un@hTejR;b>g$dt~}=W`n>xoyZOG~zZ|6@!#30JRn-L^ z7SqFl{!-5ib%pos>HPjA%;o&1p4{8#@60+_D8qC3kK0;h0L+}clow) z?dsH!^u)x4UvJ5O`dRbktLbW?8{3>01}q8Cu)AKS+N8j7 ze`(OlBcDEL+5i7ppPrdH(|h_c%i^?_ckkXk7h99LP2ttUD=QffJ-pDFc>Vdwr$vH% z?UGVbPtMFV-f=fCC34xt6_dT*2CogfXwvJUa_Gei)_axDb9e3DZNSsq+v~e**|LbZ zxP#krtE#F%EqrF?za=Flpc?@zh*RQJ8UT4Ew4=1j=DkWd@!M&`!Tyv^dNqM=U zjm?>*-qSx+*r-hQTo+$I+iU5eQ>R`9Z$D|`yXmIP<;#~>tnvyESKk=X^X*&Nty@AO z-Ak|S+_`!4s zr_!rJ;jcR;sCIUocFor~QIJ0ATn3BpM8j34W}jK#E!k|M)7`MjP^xXfW$M_av8XQI8i-Ej3zd)Rwp*Oh@qZ4ohK9 z!wX`E#rYI>ntZw{RlU>Z^G~J9y9d^u*fXWWSf=|y(e9Vst{c|Il}TH@tYQu>Sy}PU zWr178Ls>SfXV1k8cGsW3|L?8;%h#_TAGiM}7`I-0`e|Ri=}V*5E__|3&BJ(BI%CzU z2Sq#iSAD#ZvUz8+?mU)Tb94mdHneCnzd^aXR%H75Ccb z)|rK^-nf0cyVg{(-1#hy6^Sz2GS8$Kty#Nvve(iTSFn#n)k*C ziyzZlbi7xrYCHb;*!%B`GjC0Neqxi4j@aVGi<={K%0BG=_LC(mMz6QP;>dvm483l~ zcV2(3%E-=sd}=Ca#BE(@=9&<#%{z9u=uJQVv|ak4OhRv+>385Qg0rvFG^CbU%c(y3o_N!&dZ&%>Z0kRdf2oBYR(e*JSXNrFlE2B( z{$RuWzKj_UJvw$}2G*SV8tXcZBTK4wqJj8sD~HgFyv;=$j~sbsCZGFe!gcAAUo}0d zi(2mFDan~v+~GN*B%&1><|XX_w+lrwYMd`VdXaCOpwz{-tR@drv!r>`SA0<9E$gf}Z>E<`N>{cb# zzZLB~4{r-LNI$}$A@0Ol_-h1)I2jcsGh!z)rzW?hi&<~lQ zJ`UQX#m4rFldai(+TRz8`+s~m+|L*wBI$eCLq%wgRcY79kA}By-{xp_a-XN{DJ0k5 z-q*)hv{Pnj(94}?ti`$uUtUuE_4{{JW2|S^IZXi$0gfMyX{?*~?v1tk^C8*H%+h@+6tv=N&d6rstw|C27!DGktir(F^w6dDyvRLreEukZi4U2aE z3EplxU%%l0KNkJHB`d#fc2PR`^Ye2)CqGvc-^H6ZPi|~vw2?Dk88YkV&&o?LO}(U$u8;Ws(+?PVv-OLT50 z-rx4~?=`+n_fH3uv&)2Rlk9fAx%SFDsTChJxSAI#$THf?tWl3$TbP|Y`DMv8pS^uA zEpozFndo$Tzvc8}{qx}a#fRSmyIMYIaPcKr^KE4!!`t;q;eHUQn!*hFC;_W-9Qvze9*0(nuiIM7Bc1iZb<)@zm&A4Vf5_(mv z%4B+o-)-^2WQj;M_2nzBzZU6st*WZ(a5z8mHn(0~*rcO1RqJb2^T&_fbL{Kqr5N4hTAq`WQ}p{=?gGiUz|htJjU$H+F@65b z9kcI`5NPf2-o3ojPhX8{UzW+z>}Ybf^pVUy&grL1FFexxawC=J@Pm7Mt3j7~EL*<( zWLmL{QlgcWRfn50`)$RG*Tg{go^kdjj3Gk%SmhIcOMG5~rEA!+g zUPzg_9yN_2MQcBRQ2?GCwNw&lBHf7HZHY5r;1{@-$s z>GK-NwUuA;=B`@B!dw@lw{|DvDz9{tL$8|;eqt_M~T^HokBvC&JC6AyCy zb!x?-?cgNPm3uoUH+SRi-KU?=zyIXX(LlX8nbeGo3Gd$JO*&atV<$grnWCrARIjN{ z3lH48C-?ie=CfzdG^d`r|21uofzD~S>(WzBr)F){T9(RKx0C=ge%GiILC!V`xMF?DyJ zRd{$>KYr=zK^xE?*7kJiY%)0f<`9)=c z4EyoI3#GEBH%(CCoPN4>)vC3}rF+@l@Bi;!_kH*M%^Np%rbZrlT&OhBpl<(u(`yeN zDE$9-|Nr}b+v##`IWcdD@votG$23fPc7i4Cwes$sRZ{a7O{ns}rqcx`I zdk214rS<7xX{1d1uFODlT^aU2tGuqfb+WCvvs7zO?XLYdPyb`-d))ANftNs8=8mb8 z*LT}_O-)^-C*J73Z;Hg{-#g1reZTQ~{kelNvXRFEL+2d+Y0vCAX@UsroxFf6X&uKO zKXdX|7vE(q_uJ<9wu2ivHMAH*UF*Jua-H{J$!d{TxxMXT>D;47RfATF#McyZufN`X z{IOeLNJ=D=iqGXE20T)|Y)^|?gI2oeP5-@a`Ts*VZrys+%x{-cSU53h)ShnQ z(5aQ(8rA#rCu`736Z8CgM_zwjv`TBzm74$8UYB|;6`JYe){@EM_`|VKhW)U?jF~f+ zM(7xsn;*ZrI=tfT)^J0);B%J?WpZvm`}dbq#C75J?c(##x8HutHfw4USF2L5+vA;Q zH($({aQZ1|p-M#j{R3~yKA*F`9-TW8JXbu~Z~4jR-0>?fDSdo#Kc&^F@#@u3pU)@v zzuvxm`^~#|j~bNRdH48gSxTf+V9LH<-{+}#YIHT})bDn^!epSr=g5BaMa%uk~801C;zTl-|Uvd zzJ-(XU)->WpSI5Ofd$`HC(g&3*K=3z@>>C@(;_BqzyRe#dyXkhRz5-L}czDz$s}ZqQYj?EG?%%64pTL9dGG z={mFNn@>LPe)Hx{!0M}4)1pilrHNq&I1ZTTzWQpl?eCj9e-N8+T4_$(`1v`Yo$8=OX8mox+V$kiqJQt!a^B z($ce+T%MVpC7%*0*6Y@sw9#X27-$_C=zdV{oc#9zD?{epzH{m+*X`T4bMo^yZ`|0Z z0xINGBzQjDt9~!kf1I=Za8G~#M6b7-_wR2{jf|M)X&0Yy_GqEZw(Z-09M!LLwVJ!^ z;)Uqf6I24Orey|QHrent?Zycc-&?nYjz1IWbb0aSjl<=>u=O9S_I~+NBFw`UZJ%xO zN`$NRc-UXdlh2-sZQfiQvRYK;xZ|``P(64wsi%iWxYK3D)vPn;&TWl~;Y6NjYxDu9qJ?=$^cKsnP1CM$>YG&3uYVWw$3UX4sWln4CU8diL!Q-`5@s z+$=d>N^I+WYH`EQxIw5Svwt=l!?L#%a$ii5U+FS=)wQ1)$?YdgrhWZ%G5g3Z$Mvd@ z7r6AiwQxUVasJ4r1@{?(RDK=#f%eqCof27q4GGUOHV*u=}XS^wVd~o!hu!!+~?>-gOqgdj0zGtE;PX za^G&)vBPC$$dc8oyTjuuXFhzGs5H@FS!ST#^ph!@Qj8*{c=v{ip4gU~5_#(O!CQ`~$y#?RiNaoyDli_<@wDUw-v5)`x4I4Lpyz^}< zN_OD5zn5?RbncBDiPO37Zo8N=bHXyg@6|6k+AXI0EZ7yR;Qi#ZsM#gSpr=Nrt0kx1 zToW?$tT^ZPqlLR2C#ULdKe|!cYWug#AwI&>buNnYT#)a+Al;oL&GxcrYHLp4+M=zJ z4uvvn+&`(yxhlT=EKs=f#1-i^)3kWPR-IkYtt0$($Id%9{SF#7S?Y?dw=^+iX|vW7Y{Oce6p_-mvsx3nbQ|1wk;BDa-TTa74fyTwKJBvZjDMUECelT*>qE<(V-#5=p=ZaswneEjoquSY;pP4K>-GAJ_wMO}%{yvX=`Bw2gA*_)jJ@oU34Qy}Th+ z^02{;bD}eD+)<2OnIsc4{qDMunJ1r0uDtePcg%{eLr1@{=q-3-A;K%WeUX7wcfmsA zlQYk$T#?pUc4^A%(oJ94T<qyH^S%XXr`^6b)8%MSW&Jr}moXIY5ai_e0e zcI>=x(JkuwN=vhuQOynBi}fWQ{Ca8kOF40aY+Bj39glD5h;~ikDJamGsit~a#bdMY z)bL=-*&PpqMK*1<>i)^p+IH?qomBsQ_apDweHU9xJzRS)&t@f$^;M}Zmg`oxc3!&Z zrX%LKG3V4#rQJ`tqMv$gj@h{)=yOb`LoYNOY!)qF-v0Udc|i`A6HklIoH_I9X8Qck zA0B=QUVYn!e_7_lv(J9~`K-^u!n9@k_Qjc6v9WXa{C-#Jb2+7`sO#-pSBZa?^u3$`NIYrv(yw%AK_s+V6Y)~_SQRA z)2C1SdG!+4({JIr&#%wcUDmC-Y!;HrR}t%bGor@A#Kt>)dw-sY$zm3sdH!`_b)?2`)ry_ddx=1KRTw4%1~V{MXe zJRDUHbzW5Dd-L-6M;o8RDmgQkC?^{D8rV#G#k)@Yw29eat98#ehpgQgvi9=(pAl+H z7751Pw?E~*)@*9j-2dNxhOS#HUQu4z+L$uO`J}~>$0kS%dt)1VfAEb zW?>RL8`kTz)^Xp>MOn9ha9y@3;JVTQ0t{`53A5CGf9oy$@gZ^Av}rTu&VBs2-~QRJ zub}n(@1N9r)ZAYlv=Wrm3{AF7e|)@OxU=QuyLXSStPJ+?^D{Iw+<5a_X4Cxtf1XeA zQk_@%jDwk(S!FV3RaMmn9pU4J878xI^z;nP&5uvj4&PbylxwQj)4r=!Vq#)5XUziL zTYd1L(@0u9=f+UIW`?scv&*b zMd{#+7aY~!crIL=m{wRQ*xuqG_L5_0~T*VLIC+R-E~SdGr}k?>i^f z8BOq+(Bab2+I(bE;i_IX#?va%tBdO@MbDd92E9+Rn5ZDeH__{eIg`o0{`b4KMCFQQ zm8P8*)m!^4V#U?W-O{3^G56C~1*y)AUT!^gfxe)+VDE%ZHw6)u)LAOdjxzVt*Is-Q zQ^qTQ(`&Vxmo_hL3a5^mG_vrFPqHPJ@wRx;cC`Yx5b6tJC`onMYw|2o1b|8 zIcBfbxmIrRitl&7uL#k)v90#?0kgB`&wsw?E;l z&$HDvUoIZ^@$(B@e|`J*=-AlDbIb4DU~D6`?)~MGm{+;86MmcRpZvx4gH6Wv z%qz2>Xq)P-d-@~C_I$}sx4(S#1>qZW?M|%Q7?M3fJJjOt{lf+6>~1<-jNBgYm&9zVI~|>K+vgeA9CP0|sW<8V?ox*o3u2`j*q=n}dc8F}n${h& z@yu=J+7zoGm&bt|EC)V+%$c3E^32q$rF9!;r0$Gcz9o6xy7$JR>mL2SkQ%w8)|MyD zNnz=c+Whl-QGKY#eTnAEyD(E8nJ(>L?$_&lzZds{Yp|IgX;3;z9KO;EN7 z)|xtF_U!vx*S-I}W8G=>`E!(~pBCg{+o;2R|NG3DGZzMEZ@&_JrfL1T`b1~rQk{bbWEXMH7R`#ezBG5Y zNR)2ZTeG#*KWv{xEk3z(Yw0rWtG(a+rfps9)a!nFG6f{EOhF-SKTr_p13Qh)$8lJ*FPODmqxC*8o8o&bLGkA#SXooNaK*c znzZeZcJxE3$^xFl3KBe*E?@S|&(}Yh(&XIEw=w;^98)7hcDCidKcBd~y}eDOc>kuh zf;58g30V`F>8o3nI#ZhZ&V6y)Iyql>)yLXfXT@G;zc#tj6`L}f$@Eje*YDpiUcT&X zB9&ZPI`!two3ZhIDnguA*48gxzgCv&O_E?MF8(aiEuG8L*7ohv#ffQ|n%;Kvmj~_q zzJJm`HKs-e_dXdlsoqVycI~P>vwJyJ*YiDupxxl=(x`)76uclS@iU5@xY&-YlKVvCZpWd1>R-}m|B@#hhdk(Ec3MFpXta*t{3^p{0bUlxgqZSJqKQ1aJd&3dx1 zNM?H2YR}C#4{Uq;cE6d~+1^S27cW+pu`21v+IsBJA*G*xtWKXkeI?73iLtxv_OhEf zNw>}BE#EI8E+{DYb01gxVNl1Jt2J|bE)R3pQK9bc?my4W-xt2SQyCT(mSHx#ziI`@ zu@JoJa&S~gOruog3a(ZrV`H|79xY$Letmo0_p*{q-vkd8j`qVozP_8cY}s@7+R1I8 zn)>r+>9_O4!oydtj-TJr(Q$$~RBP&vvbR?De_#H8bZ6&hhL~L?GnXz^z4i7Rvu)Vc zsMMq+p~FAi_x<_g&BMIvs?^V#dD`LYjO^|GH{U$+c@KM43^X_dC%PZK|F8eV_nVi0 ztA#Ci#+fL=HZ3(VQf%GpN0s+<Fs`O@nhfq#aTU_sgbu|Gyd4u|GaX? zy4`#4ovV$U)|x40(YO2O9Tpy&^M|+Pe)?&%G>G#s!}Yro{t5yLQoWPBR5@D@-Pi_N zR;aD5eI?6O=J>(SHZsQ(%gV$qn-t1?-}c$ixA^zAqu*_9&hJk>W1hd=^!nV-cPjqn zR`Yaqbad3%KPh_sr08|bo*yrr?ZfQmKY!eB|L}3YedUYeLNKpX##GpxEo7+_`mwM7 z|CD{*qAMzyBBp`%Zsg?j{QdpC@bNJgZ|~Dnyq3l)P6^O((VF^bW_n-3EVb+FVwXqg zT*@)yWMTU9?VEwcj_G}UCJzfF?tfnyw))BE*JsY^2CNM$lwoi1Q1MAmS6{q%@sc3V zygM@##JUYFEIKl0ty#CO=;ftU(2Cvn@0I2HH*T4dnwHk~d|vgmz180*d8oX2bxMG* zz4GJH$uid8czWF?pSSznlT*fexFPv?A7~PEYqohrboAn=wM^0v=Q}7Im}gskV{>{# ziIuN{z=6*;r=A*RXJ?-|d-ihH)`Gk1qg*B19F1l!*}Z%A!i9TPgg_O^*|hGZOI0`B zd{g$o16=#Th$pi@)yQA}ywjJLVd8t<u+A-^n><-U|cn$ZU4CkVz{jm~il*?U%1#lV*wSdoS4O^1)_$fQE~WoO39o z_J{lb_JaDT!-+Xh?=FO8qu{m#Ep3mi!w6tkS8ylYgdTW@}XM*RMA7p66}r-!(N+BE?8DuIi<0(ngDIcR}mDqt>>i&g|*!&1I|o zWl{f+>1Yz?jTILzU!3^t#>Vs)FJCT<(Al(OhsVMI6`{@xSFaxZeBR#QXyzW(9u-Jg z10{>>W8`nGYybY`slXG?hs=G?AOE;_ecf-Bpasu3H(s5WnVLE^BuhCUIg(N4_|f}5 zT}K}mR#jJLn#>a6VwE|rI??08?c2_3mp!*eC8wpeZQW{`wN+{#f3I7!*V03c%)ej0 zdbMcx?(U$KTVCHdo7N55HuL;>WMt&gLYZ4{zkMsrH}|{${iDtNw9TGMg3BXxHf@`> zIBxyrtgSazgcZO3wk&g!-}09)UoKm`xcTYR)MwA0MZ})Hn77@4XR_PklNT;r_%_3M zrk~eRp_e6H6DJBDdT8+abtR~>Jet&)wz)Hvbw$|f9qUfNc)_u3B^S6>fO=a=>C@St z`}*0p*z({0%rR@bzJ|gli%+!$dwTKVlU_VOG-|Dxva+&@ z5a+JlyE9E@?Ot_dOO$lutkzagYcTfsi{Ex@!?+I{aIiJs*w*{H>dNchw_%|hE`EDi z#&dYV=FP>%1}PhN?K;&fEqpuKh^IMgYggth8JmiR5-V4=$)H5(qjqz5xj@6#t)^;f zYGthTQESE4y}ktAWBljZb!S)-2h;QN>@F0=IxX`&U~+N(*VEryU;o}>+rRDkLz|_k z(?lApo33PqZn`O&b6ZiY`{LEBz9v$4c`mDXs7-cJ66`v9sqFQ#3L8J8nG2%U2I)-8 zFqySG%Jk+o-?d?jWAq;XwwpV5Zi~~x*yCSqrbn&y+I+L-MX?D_aZvi?3C^Q>R`%TWK@bUI6T7 z81dxb$J+VJUSBM^d%Ns~KxNF$eeQQl3+6xizE(DBj{OU=od;-XY|x@zv+xkr)$Z@m?p=pk|0WWySUhXpgjR{Ji#xZ|n3 z&hf%bv)Q*pi(#&W(EE7hH`w-1dwr?I_RzG>(%J7*T;;^=9{jY^y}mfj+Tz~j-*TQ7U_0fbdkf>cJG=sYl7BafBEvInowuJ$|);Dv`nOU8GJ6Q2ytG%c+t_{ zpa1vw_xm6J`~34y+Jk4;v+*c79gv2T~>g{{4|?Rd^Cw#J6m$2WFI z+2($%l|TQvAol&b-y*O|5KQk3?|Xi+gg2|NIgzC!Vp^2_$>h`D7ry;vB0GK8W1CkG zZGyP2*d*GVSD$}=vX|=1_wUu^`h#_*f%dhV_)eR?d3TiY&UN0auP(cpvuVeU6KBqx zk^O!*!(?UH>X6k}Z-;IAxz3``dG*yr7c&gZ&5wWj^hx&m+N-aoc&SFMRh#!b@wVBt zR6!oL{F%*(5;K;0DkLOEUOPOm4b+&0V;j|+^Na6nGc?@5)4U*Yk(24C9UpIUG=%2N zJ{ByujFB~Kp2fW96VE?CnPMbkv0;9RmFwCt(87tRwdX#4GU`1R-Z(34wd>xvh0vd)vijVXJK?Rpx%Io$s~O zYi*da0LO#hc4yPN!&Y~`uDYG zjb^G$K51ZJaNyQ0sa`kc+uP)C{;@f~IN-$O(@R%gfBohA_s5lT+S=MCQoJ?y)#v(M zzI*rVfddS|!NDd{ytm$ha`VlbmVGjoPVw>b=k5Q?yng-qN>*vpK?Pe|+c$TuJye8l zidw;&cHmT9DPVJcd8_-zyUX6PcCa(G=m>isl&W;eIluUA)s@)kZI3S$#oDxHGO%XN z^O*PiV#bxo*x1y90{%{yIW_wnR)#FQn)Nnnd)VrUr=LbdMlRgGy}kDLv5upQrkoay zxxR4m;^w|Szey)qwnp(DHpt7I`X%<(s({VgrYX;kRh;UzDgVA*(ngK1zuXq6O+ImWH4$ZgOVl+2!T0f6uhiD?Pq6>$b(d^#*78 zxUSeNlGH(AY@Y<{8U8jb^IMK0E2Wc;4=Hk1a&Fq)$h!6+4+S$wQ?n zHPU3Zuh!I4Wzs9wmiceX4b~9@jjv4fXsOy;cDq)bi#07Rt;0p>Nzu;O<7=*iviQbb zyS!G0+_~y?>U*us@zk=iXU1X$Y@=@T-A49ue(azoZsURgXvz45%;gkzs;W6mG` zRf`|ZygZ2dzs-#w_at1FNC{_J}1>^S4R;`;5gD`!_{f8MzD@y~0zmu}zQy?%ZE z`Tu;!y!#&a+}hUryz;xDDag8=@&^+plvwp9N^H5E`|9hjbp>lcQ2@br><=4k;Nh4h zJvB?`@SdmK6%&6yWR|mgm)3FYCeH<%9u8jd~01&3GxB5l}e?%(&2-XYibJo$m$ zE5(z-3EHKPe%k$W=DKXtDY63OP9$9E^TTe(j~!Wul|OmR?5|=_LI?fA&|y1BH47oj z0v5PISO_wOeb_6kn8R%PZ?RkhD4db8O|9H>2k@X0vgnC&zeXvfpawH1%3?%;px{kM z;uKD7KfC|szm3bV*u~fHz2I5(L_5^dSx|7I`u)d3D}F8$IRdsGiKyIjCx71Kji>8t z?lYq{&y|#vKK-b8bI9<13uHJ4B~4gsK!yvE1vfQgQ)sisq4&J``_8Yq+u%bzPzOvr ze)+o^|EJoO0iX9;E<`p8#NPCFc8Xn%T%qH%_uiNZ?TPirTD~hQvd{zk#N>yLAxkO~ zA}75s#?&=W?w{}X?a|*HY&aWdx!*GOMxbC8letm!E`n%V^YX&SX^9O|vGQQ7WzB;IQ z{d#+z?W9u2_^JY9UhYig1r+_?yjBCWr7EA4W|9ji>%6GLnRlCIk zze`Q3%$S-h=eFS4>qGkAg(mF0yYJnWz3(gMz20R~Gk@DA#4setawR3D`@M5p{cG-j zusNSyIq!N}`LV-~Umw;le;fBDwpyTZ*6+C=_O2_}|NWKmekt$q?{_!74-PLy&qovA zp9|NWU$g#h+3KxPyHC#TUT5=Snm@nm1ID+<(>F_AGmog-ooc?n?D{IRb+4_~d0#YH zDcQDin*VnN89qpRN}|otZ1&QN84rHxo zUYUD-^L1b22-BYSi$`CbynO%uaq0X$2fg*@st9m=nE(It|C7Da!dq^?b+_H(o>nfo z>iyaDA1l9oyPXeOFn=j)YsK@q_diUY|L45BtqLshf$53MyFhLD&iB%)_rHhL-oJnI z9cNyA@3XSg^4i~{7e1TalC|&bzxO43VjHE7XKuUw_F0sLj3#Td*s{zOmrh*g01b?O zF)%Z8Gmuab>rR~IUng z(3o)gX++$)p!L@aWWERbJC?n8_b%!FzFL#nzIyTh9tHBdLoyPSe6n)0=n6)zdC&EK z?z_yg=j`pdw`||m)NOxizW(w3XItAJIc}}_we8)NZ<(+EmHpkkO|H7{@fNS8L3(1| zQ<@G=$d*`^Dbcpjp*P2nr`bWlK}9GrC1u9pcK*+Iiq9Jv8Tr)L+pi3nRAAAQ8o4D( zS8FQQ>(`abmMz;+RXV5m`MJ$owzw<}I$9drx)tgk7^i5t zD&~DRIIewPcv+?pS8K($o5^$R{z|p;%PY#zxGOUz-*^07G|yZP<4 zIk~q>_pJ(GF4tRdNOs=+yxG4$Ivg_P*ADnxx^`ii-u~dyS=O!DWp?+xjCXF!z1{bC z&Ch-HcTLW=WX|ebzV+?oM2REdBb;8oc%d-SLx6{^Fm`&_YFD$}PFhnBnY^#*^tpUw zlg_SPyH2DSsR(f@O*Ck9U;y=!XNRl~y>_igq}w&;_Ob7^OpOe`zrSC;dv~{k!ht7G zM7q1X%lGgxGynba_3Pz}6G5k4tX!#?bNg7Sd6f3i#~hvWk^qe$4UrpMwKJEUJaB+v@nYqOX|4+c5|WZ8g{|JY_uR34&2R4SpYNl_ zeBjI(oztgJSIldF|68KX(LzRTs@KKax1Dvwl!ZET;z{RWbstsQP4sAKY^(z<%sc-0$3Fkn zR~NXT1ixO;bNrJat=`QvLRh&~;-a>~l<(%<*~*PZCml49go zUe5orq{~C4DN$m_I`73BuiE9zjau85dwcgi<3@*ug$wug^!9E9H>VF9@K{+{d8kch zVPbstTzp5}-(TN;s|s*jxO*3L2+PToNdX!ii8D;3ct8DQI(_V$6>Yyb#lrq1a^Zkib+r_fj{H1a0^>*cDuYP*@dg!vBy8_NCe>Kp)a!01?re$IB z@z1x{7JvRUSN+EB!`bT|ZwZgRzB4KEAoH?JKAQtOYu3+L=2>4apTF-XbDzBZxr5E@ z9!rBvX7h%Hh5h__zTR;AC%&8=!>X6FM9f@)2@v+hmN-(RJ5 zpJ#vn`DXKZjp^DJea;p#&-(wp;IH`m_41L&h5t_1|1|#hYyE$=2ip=mgw_2%?B4&| zde4VLpa1;2{{BOH{kQr+4Uz4K4Q*|0ZGOKA1l`rcCv(F;KVQEuIaq7z$+XQi9}c$9 zv9E8OIdkUiHbtG&i`K87FaQ6?{LgPTAMZGt^yg~)U*7lo>!)>gdfs|#Ht+eP%75P^ zwhNUk%M{{kzqvV`fB)~@`_HZnUj8He|EqYW3t3ww0;`|qsy7_1kw!(;c|dwd~#KAk%K=jHr=*-0BU#P#b= z)&KJU&;I)L>n%}h-|keI>{<8cas9c|`ulqlCH`Fh|Lgvbm;V2+m)%IwZYg|oBk*3; zYhO1vw->KpgT|0^%p&9CpI=&f-m?6iP5SwHOD|^R#7_^<@bLFH2OT~t&Bn&|9_e-13>Nd_Q+PNod*0bp=L!O0R-r}ZnnyY$Vx{dGJ`A!Q3 z*6*w80xeLT9#|9F@5^M^;p{U21ywIxpQIFh;b*6weD91(ABn;+w^ z{}FxrU_;g3gMa?mtPSgSQ1H+ZdwQf(c**6=Z|`KE$p3rze9gLb4|nOEJ{`%`%A~A( zn1`Q#WBGdpZSB>!4+{KP#dPk%1p#*ccgMHob{&0GW}rUT&(Or=h;;s(hetYvRVH(; zx~g?EN9_LhPM1YrzI^%c?RLJQnb|SWUhlj+I}W~jb!**D8H<93@Oay!2bi=BCoT1Z9iOn>HP?e!r*t^{$q~iu0=9Mb0`a z_W3h+$m*+aKltm2EndC4`~JVT@}CZIp9d}O*&CN$^G9mkYoSh;j-yGSqa}Ix-YHH# z`QhjD_?$TD=`&|W-m89J3(5<8G8PB;>wlQZ*M8wN&%48rkddLUzprAFvOC}7-*)mf z9~6y^H(T^IzyEzC$#B>1-9P5u|0(M?&*t2P3vsKiN^Rb}`NlS94G|To-j9DipTBf< zs^V0yqK}W3`}q3io~u!I5$Sb%`S$I@3k#dSeE)ti%XH?fS!t=MpkuUt{r+9?^Xc>g znf3@BH?65kB3y>HwujH0;gOY>W5 zTm4e*SR@i5JVmR!=_tG;@1&L|DVZg^K@< z?*Cy3+NE5Y%F*i7;9PSip+xttl# z7FJ&n&3d=I_VVAH^5v6vg|0ouGAC(cM2Sqgnw(|mrCslrbfB*c;%j*`EpIBsNWkGZNNgG>U@AC2UGcq$f z<*mQ>#5~(-32EuYAzE*_uCO$NCUil|{jOd;3L4AP(Ft08)xdZ1yLWPNwO>Qy>i?Q3 zD=YW(_ZR>DRl00h+6JA;_wL2@$*KOKslsz|~h9dmFkQ?=X=nT^A>NpT})U z#n-FhmW7Wj-tYUZ*WLZD?1$3ilONt}K3?(pth|Ad(V~g#>HEJ(NJ~F{aPTkaps*)JuXXhF9#q=>{{H^)+U@t2 zY&Jgp=8eq$KTp@sm^bgqTGaXSrDWcn84F_cCi*P9vAcW% zr~vr*Sbu`Yk(19q`}q6c5BjMp)oWz2gZ;9}%-ORaUs`(FqR$z0EZnb){GbEw-0gn} zUd(85TYOM`{+#5ZqOPr5P1)FfRX%Ke{_|pg-I0@%)eU%>KYlFiI(kX>{mpsvBbiPY_FI}4MW!d91hr!JVC8&ls5T9feX z%xhdZH2`$(^HHNPE>BUtoWed-f!3Z_{NQGXJ5UNI-B-++25IOZfU>+W1*Q`6SWStVsD%yu7o z@xXq}yho1%r8cdAoy-zj$G= z|Nrm*()->Qzq?}zYSK-cIWtnKm#vNQVL?m$-&fcFoSgp$)KuX~KexxvZvOK_t=*s% z5s@xRf_ySJ&j0)Sd&aC;&${*3ZP>A+CHJ<=VrkL5b8{-^PLIFG@vr5u;;r1-A3qjK z^|IO8+DsgQh(23V-h=< zFyZM_ot-g#{Pur3cJ8#icCBc~J2%l8AzGd9t3fS8o9}lv@A>^Mf7|NAZ&D(~Hb(Sx zi|c>tj{nn@wYBN_PnX4k3?RQ=kE@=#a%HAo+@C4+e~!m**s`T3_jb(wy5_H6ZRXCE zKZ~bLn^qWGzWHugM@-&a7YVw?cP5h+V>wlae^Z_`0}6r{~yC+b`(tXTH5tK zKjwS+{n~!{e;;x|Cp;_r&9U&?efQa+R{!m*x$ov=Wwq`9(fxn=(xs}Wo^tJbe-XTh z$mai_Vo=-j%eQZz{{2q>d3F80hu!-7mPFjim@3BAdSiQjkNG`|&5@$%Q)=w~Z!HE7 zwZMs;;mb0GI9O^7Ud?;1E6VlLUwi7Ka}Ha!z_Vwm=J)UKna}>tF?_|& z@My-_a~o&QU4Qs(vuQc^Wr>E@7Ji$fbmP6%F55mV_#%0F(=MCy-MiNPKGL4GYxi!8 zsxKP0ws%9cL_y(X_y3Q#nE3Q<*A0Cqr)_psyX?8~huTDslUC>gw@%M!CQqje)BW>7I%1J==Zx(4J19+jaA9GyiqOZm zTm}p;OS+!VyPaVRS`u6K*2>%4d&V-){CxhyhYy2_#!E}j|9rE#H^5(S`t5yfTchR{ z?L2ekib*@ad{RZljDrVlOG=ikyqeW=Sn%7Mo1nul7Y1~E{Fv?I>w7V4Yln*xsEHY! zzxU+9X7(5F-YpB!di+~@?K~@MYe_k|q>_>;t*x$JON|aDfX+y~b4TX%>Bw*I?;l_2 z+&*K@oTRF%wgQVY4<5+Pt9&L|v{PpCg|17bi2s$PN?kM;ER8JIqO$2F^Rp1a=mb7#)*Jbo;F zJ^#67x7%V+0A0K&DAmh$ef|9hhg!Q+Hs6dDT~Nk$pHI%F;JPJBZPIb4e{Gom!Gr^> z$%hKxXixPjU3)R(3S)b9_IH-;{D*5|4?HXDf4}A2_D?IAObfCrp81L9yxZ#4%fDk= z?shRTv1M*-X>d~rQ()Vxo=qiWvwBkWmL*4CC41@;dMT2`)2eK|+_;Ws?eKlk~s4NK3;n)ULfq|fC$dn8?y1bLDl8?`zq+MGXLYHr8A zF=9%AMbG=+TcUJ}cFH_{{MhFAo6S8vJd?dtIoX=GMCtMzR+#Ho{N_fWP$$dl*OhAO z>XDI=cXKL@4?K7PTHSjyXWGk`nN0@;X8JsvW;y+Is+hRAq_p(X#fzKYz01qFtti)T zXl3POHTPbg`j!nFIua$81Znok*`AvJ=VZU;)UKG_WzCz@?H2^C6zD!`)@*nD1o(`W z$G_$F@o&owUKFV953wWqZG#kA#B4Le*Yvo2<*myO)xCnqQK@bDN|Sajsxp0VuYp+ijbYl`|# zPq*K%Iwxpl$chlH@*R27iK`-X+MZW#x&1Be>lB5F9x34xhd=e*2t=G?i79x57By>2Tm_MWcyUaJ3f)$I2V*B_JYSHH2pUVf%eTiNcTM~zR z?}ak$H*Z>=oA7DJaBDwB>pHdLC(u`;CV-8(&<)1Y%ylQwSAo-*`%F6n``z!hON7=o7^XACIMD2Mtl|sLN|DKo{S@Y+k z`^N_dnWUwp0FxO1ndwY9bS!M42Z z(yw2?{`l{=`}5rG{Y`q_-rg5;%#w587V7Bg9*zJ1b$@@q{l6D$??+Ek*|=qkha^g{ycyG`1ASw9q#>d zy*wT&o-R%Z>r0!$){Nv5+-ygrbySwoDxw#QhQC;QtD%I_Nz0gM7eTyLeZ_+X34ruOI3fkv2c<~=OT?qH#etm-1JF9&b}^4lBcWV#{K*KQ>VIb+_XvP z`}_O-9V(MlZ24GAlgrD^!@}f_{`yt*=Iz_?e`S+YI-8rDE57CYT{1U(YTWx5v37NT zRQCP*_1|rIu+N%T%#iDW4Lm2Q1g^fS6n zkk*X0-qk6_t7rBb<}XN#ah+M&Z+honX2tA>?++UI*6;tfytv}Vjk>Q|f1g+V{p-Yatd-b)-ruv&s)Y@w&7%tws7gzK3YWT+8yQMQ{ z&0*1Ab~S5jQgX7fnfdldPft%j{E4s_VT3CQq#CS6_uB|($mw;_x*mCSYmbe#HO4#r8&8|N3UHAb8)Hp|Myd2X#YFU z(~I=?|IwPH(s}mm+KjDH6Q5UtV&&Jb%|G7e{|^UsaXv1$|NFAH;nc5RZYHzOMnpwf zaq54$9b3BlE-1k-(s*>Y{C=-|{hyCZw9cP8<@M)P`2Kl6-rRay_U8Tj!*8cc9{=b@>-Jjx|{QUi) zo=Y!g9JzJt@s85fa@+Io_sQ4S)Lojl>hjAYzkXHyd1n4!V)pFW8~5z_6n1*$zPR<` z)z#G>ci;bO_wL@_>X_E&rQJ*1`{j<>|GoV7MM+mgbajoBwUD)xz)DAt^Co%HmTdmFA~6g}Lm!l(+r=`P0+&|LxiSy+&QB%Yy3) z&vVOTtyxK@zH%4d3OKXk?_|T88Qyw({rh$Mx>?HtMUF-%PWZQZ&Gpyn2U}LX_MY_Q$-YS{oi^u{&!?YW@wMvi zi;`XWf7O0p^~l<4vq{H(=l*}zvfqENt;-j57Y)4k=lPBO|NniwWMb8$iz zaD`-?|FN2IU4HgVHfEE|qg7>p=DNGQO$Y7Ldw+NL_x01Kz5BOZ#q)n`?x#UFPsFVBvb+g_D*{C483!p2z=5%O1= zvO<>53cQ-F#+sJKcjZhN)05oGY*}o_kA*ImG>tD^Q#du(Pew+@qDZ@qJm<)1UIzhUG8M{Iy2s z^m=~2xW9-0{X2KK8C1H)&Riy*-=4W@)kT*czSBKX>!s{JZ*xf(U9obOeE8C|Sszzj z@J;PVOIX*_@uF<^+(jAhuVrm@n|IwRa@yYMsW1O_q$K^8VPLqY762)ZuZP?F1h2oo zIPCQ7$Z2_{yXW4Qk=gUBeed44?!9i0LGjjeopIK~F#phpv-TC{&T9+pJumcWhP6P^ zyvA7vzecXQdghYds-X4PUvG_CdpJrre|hFNt@EGbW-ZH{l~%4i$;4A>Zh<@l1CPK8 zMu-R1Jtx&wFJE!>Q{1$t(SjfUcP;w7fA-m9>A!zl^Go+0tJ{6ouIJ;_Ot$N*Z&`)5 zW-jBr5n?%6OKUmr6_+0V%{^IvP4q;JSN*-b=DA#$oQ#ajo|>OY*_kiDYy1)a~B5 z@!$RJ+qZ}D^YQWZXhrt_f763ewUZbR`Hy-%=4aqJa1dhuVe-W^O>+CKzutb5N@v+_*=wmYp1JeX zS~J{Q^W7{%S18+`J?G4JTiaJkKJVz1o|}4Y&)Sby6^cxFdF=HApPnCbJ}y%QFKTx! zx}38wN8R(5X4j*ubFH^u`?Tj@jgis!8B(Tb}@J`wAp7ZtK}*2A5iQ?uCY4`0o?B>4I7PMy9(u9Yq+-{0M>etxi-UH;zQ>hJMsk#859 zoVDxnsGGR#WV1@=?$nt}1XE|J1aF_k#IPgmlTCm1_jl^YMYB$Yt(KhI#|16mJogB? zX`Wsp9$0qTXjbaY-wUs6oA0{&Zu6py^`^7e&-?S?F#q|kMG`z0zC>GxhAvsjbv|UR z`_ZDSH%#8nP0RedWx^|s=aMTVP0d$6z5L*Y&ca!NjI#nAXHPEh-5a<5X=UB(Yipyu zzgE?1D&;NDTpPDO-Mez${>jUJ21PF0zF8+LHIgsv=**}4(p5JwGsH+mPE&oqr&!t?<)7FNqepAN%^fx$I6E?a2{r;u%;3O5#D#5@nReQsB->nT5X=hK3+;)1?Wv#AF zSM$Wr=54>4Wg>M};{LUjS|-Np&IX6*g!i6}nwfT~R{zca&^hG`mSxq2Or3tYE$i3o z!&fuiSaaH%O%Kms61Mv4v}w~`MV7q|+dJ>tWs|eVS+ed)XrKPH<$TlvHijK8nn7sv&tb@bXlk2>BrB~GT)ZyCC=*D%$<2gXw^)$1Y`ddX$6%bS7()Uy5%l^_#V zq}?%_++=h)&pvDRs%#lEJr5SJ^B5QmPOV@#yX3;D2fqp&mSxQkyzO^ueb&xbzr^l7 z%bfLd=G@X1$rq<)u}xQJjXV~*Y35VocM*c?SKkSVy}abNlWN2bqMNP=jS($zIHNEM9Gt*}M+&Ra+Ao-e?>EYzXx9obQXN&Cebk8x*o{@Ix zXWRC*N;Z%>hJk@+wyB4a_ky&FJ?186>(*YjSv$Aw`095LgF=?Qv|ev;e36rB^XtoD z%O%b2SGHbWG0kuCE#KZrxW7B~T}HW(NhPG<%-_6HuHx%Kbm9cBTc$(O?xONzgn zspe3ux#|*+)p>(sO3tR2L)OmR=6hua!`USt&TP?LeXO@vSqJ8d>-V=XPONM;eH>+U zHmWsknwj5cmJB@`AK}7(jlG*!gc$Fz@qdvvYr@$TkNt~tyE(4;C{@7pPCs86xwceOT{T zEL0zZc>cYFlPkK6E}!XN?GbEhcK*+boq@@l&*@IciZ$c%AWkT&a->E$!N#`P~_o+g~wa^22fBki6-?<*6S zqD8t&0mga%bPgMZ>-h*5mTF)5GBCw|wrt1q=Nh_tH2Q&vD)G)U7R3X`TCo z$6FT8`aCQCOC8LZucrADPq$pn5fh!Xb5r81j@QD^1XjTk$U5%CS=?o^%`%toTwcUv zu~{bj;%}IT%@4b><~+0f503u(4##!AmxkTeD~g92+~6oKyD9RD-SnWUi{b@t=d>hC zbK`8Fk_D-AOY0uGwPz}=tG_y>@?HMD^rE}-RbjF}=axR5<@&x_^KOg4iOlyOZRTC{ zt>A~s9{8e{EIP~eUH!_tQxw_*AZ|?^7r1B=P>`r$fgU{?EIEE ztE1z~BK-$*?siP)f*Mr7mgn>C(Xz0JohN?pTX|jvDm>%9Yg=TV&xB?EJ06>!hxzuw ziC*rDJi9V==R=h-NW2HRKO|N1XfZ6f^17tu7f7mE7)hUp<&_<)SkjlNC>U9vhsExW ztd`6)1(~96Hpx(<9_VnKoHG&RJhl&yS)qc|p%nhv- +mail-contacts-reader-hint = contacts and their keys are listed alongside +mail-list-count-one = { $count } message +mail-list-count-other = { $count } messages +mail-list-shown-of-total = { $shown } of { $total } ## Compose @@ -181,6 +250,8 @@ compose-add-header-button = + header compose-send-button = Send compose-header-placeholder = Key: value compose-header-remove-tooltip = Remove this header +compose-body-placeholder = message +compose-anonymous-label = anonymous — omit my address ## Contacts — trust state shown in the sidebar and the detail pane @@ -201,8 +272,20 @@ contact-meta-line-no = { $state } · server not pinned detail-key-label = key detail-fingerprint-label = fingerprint +detail-address-label = address detail-server-label = server contact-server-key-offered = this server presents +contacts-server-pinned-badge = pinned +contacts-server-not-pinned-badge = not pinned +contacts-history-badge = { $count } previous +contacts-import-empty-hint = no contacts yet — import a smol:// address above, or write to someone +contacts-refresh-button = re-resolve +contacts-accept-token-title = accept token +contacts-accepted-hint = This contact's mail lands in your main tier. +contacts-unaccepted-hint = This contact's mail lands in requests until accepted. +contacts-history-title = previous keys ({ $count }) +contacts-history-hint = Replaced by a signed rotation chain. Mail signed with these was sent before the change; rotation is not revocation, so a stolen key can still rotate onward. +contacts-history-replaced = replaced { $time } ## Settings — tab switcher labels @@ -219,9 +302,18 @@ settings-field-fingerprint = Fingerprint settings-field-uri = URI property-row-copy-tooltip = Copy the { $field } settings-recovery-secret-title = Recovery secret +settings-secret-reveal-button = reveal +settings-secret-hide-button = hide settings-secret-reveal-tooltip = Reveal the recovery secret settings-secret-hide-tooltip = Hide the recovery secret settings-secret-copy-tooltip = Copy the recovery secret +settings-secret-warning = Anyone who holds this can read your mail and write as you, and losing it loses the address with it — there is no reset. Keep it in a password manager. +# A copy button's own label, flashed on click instead of a toast. +copy-button-idle = copy +copy-button-success = copied +copy-button-failed = failed +settings-not-registered-value = not registered yet +settings-uri-claim-hint = claim an address to get one ## Settings — Server tab @@ -247,10 +339,12 @@ backup-import-dialog-title = Import backup ## Settings — Advanced tab +advanced-trust-section-title = trust advanced-rotations-title = Rotations advanced-rotations-description = Issues a fresh key from your secret with a signed certificate proving it replaces this one. Contacts accept the change from that chain, and the old key stays readable. Rotation is not revocation. advanced-rotations-completed = Completed advanced-rotate-button = Rotate identity +advanced-rotate-needs-account-error = rotate needs a registered account advanced-first-contact-title = First contact advanced-first-contact-description = A recipient who isn't a contact yet, and whose address carries no key, has to be looked up on their own server. That answer is only as good as the server: one with no pinned key can return a key of its own and read what you send. advanced-strict-pinning-title = Require a pinned server @@ -264,6 +358,8 @@ advanced-logout-button = Sign out and delete all data ## The app name itself ("carrier") is a brand, not translated prose, and ## is left as a literal in code — see the extraction report. +onboard-welcome-title = welcome +onboard-welcome-description = Encrypted mail with no account and no password. Your identity is one secret, kept in this browser and nowhere else. onboard-tagline = smolmail: one seed, sealed mail, five operations onboard-create-button = Create a new identity onboard-restore-button = Restore an existing identity from a seed @@ -271,12 +367,14 @@ onboard-created-title = Write this seed down onboard-created-description = It's the only secret, and it's shown once. onboard-saved-button = I saved it onboard-restore-title = Restore from a seed +onboard-restore-description = Paste the secret you saved. Your keys come back from it — it is all this client needs. onboard-seed-entry-title = Seed: 32 bytes, base32 or hex onboard-restore-go-button = Restore ## Signup signup-server-title = Set up the server +signup-host-label = host signup-fetch-key-button = Fetch the server's key signup-continue-button = Continue signup-start-over-button = Start over @@ -302,7 +400,9 @@ delete-dialog-body-kept = Deleting it here leaves the server's copy in place. De delete-dialog-body-gone = This removes the only copy, which is sealed and stored on this device. delete-dialog-everywhere-button = Delete everywhere rotate-dialog-title = Rotate the identity? +rotate-dialog-confirm-button = Rotate reset-dialog-title = Sign out and delete all data? +reset-dialog-confirm-button = Sign Out # `$list` is the sender's own frontmatter rows, already typed by a human # and reproduced verbatim — not translatable prose itself, just data # interpolated into this one sentence. @@ -327,6 +427,7 @@ status-busy-fetching = fetching… status-busy-refreshing = refreshing… status-busy-registering = registering… status-busy-restoring = restoring… +status-busy-rotating = rotating… status-fetching-progress = fetching… { $count } in inbox ## First contact / server trust @@ -335,3 +436,14 @@ first-contact-blocked-error = { $address } isn't a contact yet, and { $host } is server-key-offered-note = The server presented this key. Pin it only if the fingerprint matches what the operator published. server-key-already-pinned-note = This server's key is already pinned. server-key-pinned-note = Server key pinned +connect-unpinned-warning = { $host } is not pinned; its key is { $key }.{"\u000A"}Lookups from this session are UNVERIFIED. +recall-rotation-not-found-error = the key bound to { $address } is not derived from this master within { $max } rotations +contacts-refresh-embedded-key-error = that address already carries a key; use import instead +# Shared by a first send to a never-before-seen recipient and a contact's +# own "re-resolve" button — both land here the same way, by RESOLVE finding +# no existing key for the address. +contacts-resolved-note = { $address } pinned (trust on first use) +contacts-resolved-note-unverified = { $address } pinned (trust on first use, UNVERIFIED server) +contacts-refresh-unchanged-note = { $address }: key unchanged +contacts-refresh-rotated-note = { $address } rotated its key; a signed chain confirms it.{"\u000A"}now { $key } +contacts-refresh-no-chain-warning = { $address } presents a different key with no valid rotation chain.{"\u000A"}Verify out of band, then import the new smol:// address. From 32e2e2a8f24c21c7bef7b2c9225b5c71f3d64c92 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 14 Oct 2026 17:39:35 +0300 Subject: [PATCH 34/37] feat: add multiple accounts with a sidebar switcher and per-account folders --- locales/en-US/main.ftl | 20 ++- src/app.rs | 388 +++++++++++++++++++++++++++++++++++++---- src/demo.rs | 52 +++++- src/main.rs | 11 +- src/ops.rs | 172 ++++++++++++++++-- src/view.rs | 127 +++++++++++++- 6 files changed, 702 insertions(+), 68 deletions(-) diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index b7c1612..1315e25 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -99,11 +99,13 @@ draft-saved-note = Draft saved ## Log out logged-out-note = Signed out. Create a new identity or restore a seed. +logged-out-switched-note = Signed out. You're now using { $address }. ## Onboarding identity-created-note = Identity created. Write the seed down. It's the only secret. seed-restored-note = Seed restored. Bind a username or recall an address. +seed-reused-note = Secret copied. Bind a username or recall an address. ## Signup @@ -350,8 +352,8 @@ advanced-first-contact-description = A recipient who isn't a contact yet, and wh advanced-strict-pinning-title = Require a pinned server advanced-strict-pinning-subtitle = Stop that send and offer to pin the server's key first, so you can check it against what the operator published. With this off, the key's taken on trust, and the send is only reported as unverified afterwards. advanced-logout-title = Sign out -advanced-logout-description = Removes this identity from this app entirely: the secret, pins, contacts, and every cached message. Without a backup of the secret, you can't get back in. Mail already fetched here may have no other copy anywhere, unless "Leave mail on server" was on. Whatever's still on the server comes back on the first fetch after you sign back in. -advanced-logout-button = Sign out and delete all data +advanced-logout-description = Removes this account from this app: the secret, pins, contacts, and every cached message. Other accounts stay as they are. Without a backup of the secret, you can't get back in. Mail already fetched here may have no other copy anywhere, unless "Leave mail on server" was on. Whatever's still on the server comes back on the first fetch after you sign back in. +advanced-logout-button = Sign out and delete this account's data ## Onboarding ## @@ -359,7 +361,7 @@ advanced-logout-button = Sign out and delete all data ## is left as a literal in code — see the extraction report. onboard-welcome-title = welcome -onboard-welcome-description = Encrypted mail with no account and no password. Your identity is one secret, kept in this browser and nowhere else. +onboard-welcome-description = Encrypted mail with no password. Each identity is one secret, stored only on this device. onboard-tagline = smolmail: one seed, sealed mail, five operations onboard-create-button = Create a new identity onboard-restore-button = Restore an existing identity from a seed @@ -371,6 +373,16 @@ onboard-restore-description = Paste the secret you saved. Your keys come back fr onboard-seed-entry-title = Seed: 32 bytes, base32 or hex onboard-restore-go-button = Restore +## Accounts — the switcher on the sidebar identity row, and adding one + +accounts-menu-tooltip = Switch account +accounts-add-button = Add account +# Shown when signup is about to bind an address another account in this app already uses. +accounts-duplicate-error = { $address } is already in carrier. Switch to it from the account menu. +accounts-add-cancel-button = Cancel adding account +onboard-reuse-title = Use the secret from another account +onboard-reuse-description = Both addresses will share one public key, so anyone who sees both can tell they belong together. + ## Signup signup-server-title = Set up the server @@ -401,7 +413,7 @@ delete-dialog-body-gone = This removes the only copy, which is sealed and stored delete-dialog-everywhere-button = Delete everywhere rotate-dialog-title = Rotate the identity? rotate-dialog-confirm-button = Rotate -reset-dialog-title = Sign out and delete all data? +reset-dialog-title = Sign out and delete this account's data? reset-dialog-confirm-button = Sign Out # `$list` is the sender's own frontmatter rows, already typed by a human # and reproduced verbatim — not translatable prose itself, just data diff --git a/src/app.rs b/src/app.rs index 4ccc2d4..986132e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,6 +60,16 @@ impl ReadMarks { } } +/// One account folder, as the switcher lists it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountEntry { + pub id: String, + /// The registered address; None until signup completes. + pub label: Option, + /// Whether the folder holds a master another account can reuse. + pub has_key: bool, +} + /// Onboarding: create a master or restore one from a pasted seed. pub struct Onboard { pub mode: OnboardMode, @@ -202,6 +212,16 @@ pub enum Message { OnboardSeedChanged(String), OnboardRestoreFromSeed, OnboardSeedSaved, + /// Copies the master of the named account into this one, so both + /// addresses share a key. + OnboardReuse(String), + + // Accounts + AccountSwitch(String), + AccountAdd, + /// Abandons an account that's still being set up and returns to + /// `Carrier::previous`. + AccountAddCancelled, // Signup SignupUserChanged(String), @@ -346,7 +366,14 @@ pub enum PendingOp { } pub struct Carrier { + /// The config root holding `accounts/` and `active.txt`. + pub root: PathBuf, + /// The active account's id and folder. + pub id: String, pub dir: PathBuf, + pub accounts: Vec, + /// The account to return to when one still being set up is cancelled. + pub previous: Option, pub store: Arc, pub master: Option<[u8; KEY_LEN]>, pub marks: ReadMarks, @@ -398,7 +425,36 @@ pub struct Carrier { } impl Carrier { - pub fn boot(dir: PathBuf) -> (Carrier, Option) { + /// Opens the account `active.txt` names, else the first one, else a + /// fresh empty one. + pub fn boot(root: PathBuf) -> (Carrier, Option) { + let id = match ops::load_active(&root) + .or_else(|| ops::list_accounts(&root).into_iter().next()) + { + Some(id) => id, + None => match ops::new_account(&root) { + Ok(id) => id, + Err(e) => { + eprintln!("cannot create an account in {}: {e}", root.display()); + std::process::exit(1); + } + }, + }; + Carrier::boot_account(root, id) + } + + pub fn boot_account(root: PathBuf, id: String) -> (Carrier, Option) { + let dir = ops::account_dir(&root, &id); + if let Err(e) = ops::save_active(&root, &id) { + eprintln!("{e}"); + } + let accounts = load_accounts(&root); + // An unregistered account next to registered ones is one being + // added, even after a restart, so it always offers a way back. + let previous = accounts + .iter() + .find(|entry| entry.id != id && entry.label.is_some()) + .map(|entry| entry.id.clone()); let store = match Store::open(&dir.join("carrier.db")) { Ok(store) => Arc::new(store), Err(e) => { @@ -416,9 +472,14 @@ impl Carrier { }; let drafts = ops::load_drafts(&dir.join("drafts.txt")); let settings = ops::load_settings(&dir.join("settings.txt")); - let app = Carrier { + let previous = previous.filter(|_| !registered); + let mut app = Carrier { marks: ReadMarks::load(dir.join("read.txt")), + root, + id, dir, + accounts, + previous, store, master, page, @@ -459,12 +520,29 @@ impl Carrier { compose_fresh: Cell::new(false), form_fresh: Cell::new(true), }; - let op = registered.then(|| PendingOp::Snapshot { - read: app.marks.snapshot(), - }); + // Through the busy guard, so a switch can't land while this + // snapshot is still loading the old account's lists. + let op = if registered { app.refresh() } else { None }; (app, op) } + /// Accounts other than this one whose master a new account can reuse. + pub fn reusable_accounts(&self) -> Vec<&AccountEntry> { + self.accounts + .iter() + .filter(|entry| entry.id != self.id && entry.has_key) + .collect() + } + + /// Whether another account already holds this address. Two stores + /// fetching one mailbox would split its mail between them. + fn address_taken(&self, addr: &Address) -> bool { + let short = addr.short(); + self.accounts + .iter() + .any(|entry| entry.id != self.id && entry.label.as_deref() == Some(short.as_str())) + } + fn master(&self) -> [u8; KEY_LEN] { self.master.unwrap_or([0u8; KEY_LEN]) } @@ -506,7 +584,7 @@ impl Carrier { return; } self.drafts.push(Draft { - id: ops::draft_id(), + id: ops::random_id(), to: compose.to, subject: compose.subject, body: compose.body, @@ -536,27 +614,51 @@ impl Carrier { self.compose_fresh.set(true); } - /// Factory reset: close the store, erase the config directory, then - /// boot a fresh model in place. The connection is swapped out before - /// the files go because Windows refuses to remove a file a live - /// connection holds, and the busy guard has already seen to it that - /// no operation holds a clone of the store. - fn factory_reset(&mut self) { - let store = match Store::open_in_memory() { - Ok(store) => Arc::new(store), - Err(e) => { - self.fail(e.to_string()); - return; - } + /// Deletes the active account's folder, then boots `next`, or + /// whichever account `boot` picks. The connection is swapped out + /// before the files go because Windows refuses to remove a file a + /// live connection holds, and the busy guard has already seen to it + /// that no operation holds a clone of the store. + fn remove_active(&mut self, next: Option) -> Result, String> { + self.store = Arc::new(Store::open_in_memory().map_err(|e| e.to_string())?); + ops::remove_account(&self.root, &self.id)?; + let root = self.root.clone(); + let (app, op) = match next { + Some(id) => Carrier::boot_account(root, id), + None => Carrier::boot(root), }; - self.store = store; - if let Err(e) = ops::factory_reset(&self.dir) { - self.fail(e); - return; - } - let (app, _) = Carrier::boot(self.dir.clone()); *self = app; - self.note(i18n::t("logged-out-note")); + Ok(op) + } + + /// Signing out removes this account only; the app moves on to another + /// one, or back to onboarding when it was the last. + fn factory_reset(&mut self) -> Option { + match self.remove_active(None) { + Ok(op) => { + match self.store.account().ok().flatten() { + Some(addr) => self.note(i18n::t1( + "logged-out-switched-note", + "address", + addr.short(), + )), + None => self.note(i18n::t("logged-out-note")), + } + op + } + Err(e) => { + self.fail(e); + None + } + } + } + + /// Replaces the model with another account's, keeping nothing of this + /// one but what's already on disk. + fn switch_to(&mut self, id: String) -> Option { + let (app, op) = Carrier::boot_account(self.root.clone(), id); + *self = app; + op } /// Cancelling out of a not-yet-registered identity removes the key @@ -846,6 +948,52 @@ impl Carrier { } } _ if self.busy.is_some() => return Vec::new(), + Message::AccountSwitch(id) => { + if id == self.id || !self.accounts.iter().any(|entry| entry.id == id) { + return Vec::new(); + } + self.stash_draft(); + return self.switch_to(id).into_iter().collect(); + } + Message::AccountAdd => { + self.stash_draft(); + match ops::new_account(&self.root) { + Ok(id) => { + let previous = self.id.clone(); + self.switch_to(id); + self.previous = Some(previous); + } + Err(e) => self.fail(e), + } + } + Message::AccountAddCancelled => { + let Some(previous) = self.previous.clone() else { + return Vec::new(); + }; + return match self.remove_active(Some(previous)) { + Ok(op) => op.into_iter().collect(), + Err(e) => { + self.fail(e); + Vec::new() + } + }; + } + Message::OnboardReuse(id) => { + if id == self.id { + return Vec::new(); + } + let source = ops::account_dir(&self.root, &id).join("carrier.key"); + let copied = ops::load_key(&source) + .and_then(|master| ops::save_key(&self.key_path(), &master).map(|()| master)); + match copied { + Ok(master) => { + self.master = Some(master); + self.page = Page::Signup; + self.note(i18n::t("seed-reused-note")); + } + Err(e) => self.fail(e), + } + } Message::OnboardCreate => { let master = ops::new_master(); if let Err(e) = ops::save_key(&self.key_path(), &master) { @@ -913,6 +1061,14 @@ impl Carrier { self.fail(i18n::t("signup-enter-user-host-error")); return Vec::new(); }; + if self.address_taken(&addr) { + self.fail(i18n::t1( + "accounts-duplicate-error", + "address", + addr.short(), + )); + return Vec::new(); + } let invite = if self.signup.invite.is_empty() { None } else { @@ -928,6 +1084,14 @@ impl Carrier { self.fail(i18n::t("signup-enter-user-host-error")); return Vec::new(); }; + if self.address_taken(&addr) { + self.fail(i18n::t1( + "accounts-duplicate-error", + "address", + addr.short(), + )); + return Vec::new(); + } if !self.start("restoring") { return Vec::new(); } @@ -1351,7 +1515,7 @@ impl Carrier { } Message::ResetConfirmed => { self.dialog = None; - self.factory_reset(); + return self.factory_reset().into_iter().collect(); } Message::RotateConfirmed => { self.dialog = None; @@ -1394,6 +1558,8 @@ impl Carrier { // address is bound and it's safe to leave the page. if self.page == Page::Signup { self.page = Page::Mail; + self.accounts = load_accounts(&self.root); + self.previous = None; } self.refresh() } @@ -1453,6 +1619,20 @@ impl Carrier { } } +fn load_accounts(root: &std::path::Path) -> Vec { + ops::list_accounts(root) + .into_iter() + .map(|id| { + let dir = ops::account_dir(root, &id); + AccountEntry { + label: ops::account_label(&dir), + has_key: dir.join("carrier.key").exists(), + id, + } + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -1463,6 +1643,136 @@ mod tests { app } + /// A registered account folder under `root`, with its own master. + fn registered(root: &std::path::Path, address: &str) -> String { + let id = ops::new_account(root).unwrap(); + let dir = ops::account_dir(root, &id); + ops::save_key(&dir.join("carrier.key"), &ops::new_master()).unwrap(); + let store = Store::open(&dir.join("carrier.db")).unwrap(); + store + .set_account(&Address::parse(address).unwrap()) + .unwrap(); + id + } + + /// Two registered accounts, booted into `alice`. + fn two_accounts() -> (Carrier, String, String) { + let root = tempfile::tempdir().unwrap().keep(); + let alice = registered(&root, "alice@example.org"); + let bob = registered(&root, "bob@example.org"); + ops::save_active(&root, &alice).unwrap(); + let (app, _) = Carrier::boot(root); + (app, alice, bob) + } + + #[test] + fn boot_opens_the_active_account_through_the_busy_guard() { + let (app, alice, _) = two_accounts(); + assert_eq!(app.id, alice); + assert_eq!(app.page, Page::Mail); + assert_eq!(app.accounts.len(), 2); + assert!(app.previous.is_none()); + // The boot snapshot holds the guard, so no switch can overtake it. + assert_eq!(app.busy.as_deref(), Some("refreshing")); + } + + #[test] + fn switching_keeps_each_accounts_drafts_and_settings_apart() { + let (mut app, alice, bob) = two_accounts(); + app.busy = None; + app.handle(Message::StrictPinningChanged(true)); + app.handle(Message::ComposePressed); + app.handle(Message::ComposeSubjectChanged("half written".into())); + + let ops = app.handle(Message::AccountSwitch(bob.clone())); + assert!(matches!(ops.as_slice(), [PendingOp::Snapshot { .. }])); + assert_eq!(app.id, bob); + assert_eq!(ops::load_active(&app.root).as_deref(), Some(bob.as_str())); + assert!(app.compose.is_none()); + assert!(app.drafts.is_empty()); + assert!(!app.strict_pinning); + + app.busy = None; + app.handle(Message::AccountSwitch(alice.clone())); + assert_eq!(app.drafts.len(), 1); + assert_eq!(app.drafts[0].subject, "half written"); + assert!(app.strict_pinning); + } + + #[test] + fn switching_waits_for_the_running_operation() { + let (mut app, alice, bob) = two_accounts(); + app.busy = Some("fetching".into()); + assert!(app.handle(Message::AccountSwitch(bob)).is_empty()); + assert_eq!(app.id, alice); + } + + #[test] + fn adding_an_account_can_reuse_a_secret_or_be_cancelled() { + let (mut app, alice, _) = two_accounts(); + app.busy = None; + app.handle(Message::AccountAdd); + assert_eq!(app.page, Page::Onboard); + assert_eq!(app.previous.as_deref(), Some(alice.as_str())); + assert_eq!(app.accounts.len(), 3); + assert_eq!(app.reusable_accounts().len(), 2); + let added = app.dir.clone(); + + app.handle(Message::OnboardReuse(alice.clone())); + assert_eq!(app.page, Page::Signup); + let alice_master = ops::load_key(&ops::account_dir(&app.root, &alice).join("carrier.key")); + assert_eq!(app.master, alice_master.ok()); + assert_eq!(ops::load_key(&added.join("carrier.key")).ok(), app.master); + + app.handle(Message::AccountAddCancelled); + assert!(!added.exists()); + assert_eq!(app.id, alice); + assert_eq!(app.page, Page::Mail); + assert_eq!(app.accounts.len(), 2); + } + + /// Quitting halfway through adding an account must not strand the + /// user in onboarding with no way back. + #[test] + fn an_unfinished_account_offers_a_way_back_after_a_restart() { + let (mut app, alice, _) = two_accounts(); + app.busy = None; + app.handle(Message::AccountAdd); + let (rebooted, _) = Carrier::boot(app.root.clone()); + assert_eq!(rebooted.page, Page::Onboard); + assert_eq!(rebooted.previous.as_deref(), Some(alice.as_str())); + } + + #[test] + fn an_address_another_account_holds_is_refused() { + let (mut app, _, _) = two_accounts(); + app.busy = None; + app.handle(Message::AccountAdd); + app.handle(Message::OnboardCreate); + app.handle(Message::OnboardSeedSaved); + app.signup.user = "bob".into(); + app.signup.host = "example.org".into(); + assert!(app.handle(Message::SignupRegister).is_empty()); + assert!(app.status_error); + assert!(app.handle(Message::SignupRecall).is_empty()); + assert!(app.busy.is_none()); + } + + #[test] + fn signing_out_of_one_account_moves_to_the_other() { + let (mut app, alice, bob) = two_accounts(); + app.busy = None; + let alice_dir = app.dir.clone(); + app.handle(Message::ResetPressed); + let ops = app.handle(Message::ResetConfirmed); + assert!(matches!(ops.as_slice(), [PendingOp::Snapshot { .. }])); + assert!(!alice_dir.exists()); + assert_eq!(app.id, bob); + assert_eq!(app.page, Page::Mail); + assert!(app.accounts.iter().all(|entry| entry.id != alice)); + assert!(app.dir.join("carrier.key").exists()); + } + /// Results clear the busy flag instead of deadlocking: this is the /// regression test for the guard bug the Iced version shipped with. #[test] @@ -1753,7 +2063,7 @@ mod tests { app.handle(Message::StrictPinningChanged(true)); app.handle(Message::KeepMailChanged(true)); app.handle(Message::TimeoutChanged("90".into())); - assert!(dir.join("settings.txt").exists()); + assert!(app.dir.join("settings.txt").exists()); let (rebooted, _) = Carrier::boot(dir); assert!(rebooted.strict_pinning); @@ -1795,12 +2105,13 @@ mod tests { #[test] fn factory_reset_erases_the_files_and_returns_to_onboarding() { let dir = tempfile::tempdir().unwrap().keep(); - let (mut app, _) = Carrier::boot(dir.clone()); + let (mut app, _) = Carrier::boot(dir); + let account = app.dir.clone(); let master = ops::new_master(); - ops::save_key(&dir.join("carrier.key"), &master).unwrap(); + ops::save_key(&account.join("carrier.key"), &master).unwrap(); app.master = Some(master); - std::fs::write(dir.join("read.txt"), "ab").unwrap(); - std::fs::write(dir.join("drafts.txt"), "draft2 id 1 -\n\n\n\n").unwrap(); + std::fs::write(account.join("read.txt"), "ab").unwrap(); + std::fs::write(account.join("drafts.txt"), "draft2 id 1 -\n\n\n\n").unwrap(); app.signup.host = "typed-before-the-reset.example".into(); app.signup.user = "alice".into(); app.handle(Message::StrictPinningChanged(true)); @@ -1828,14 +2139,15 @@ mod tests { assert!(app.signup.user.is_empty()); assert!(app.form_fresh.get()); for name in ["carrier.key", "read.txt", "drafts.txt", "settings.txt"] { - assert!(!dir.join(name).exists(), "{name} survived the reset"); + assert!(!account.join(name).exists(), "{name} survived the reset"); } + // The last account's folder goes too; boot starts a fresh one. + assert!(!account.exists()); + assert_ne!(app.dir, account); // Preferences go with everything else, so the rebooted model is // back to its defaults rather than the logged-out user's. assert!(!app.strict_pinning); - // The store is reopened by boot, so a fresh empty db is the one - // file a reset legitimately leaves behind. - assert!(dir.join("carrier.db").exists()); + assert!(app.dir.join("carrier.db").exists()); assert!(app.store.account().unwrap().is_none()); } @@ -1848,11 +2160,11 @@ mod tests { let (mut app, _) = Carrier::boot(dir.clone()); app.handle(Message::OnboardCreate); assert!(app.master.is_some()); - assert!(dir.join("carrier.key").exists()); + assert!(app.dir.join("carrier.key").exists()); app.handle(Message::OnboardBack); assert!(app.master.is_none()); - assert!(!dir.join("carrier.key").exists()); + assert!(!app.dir.join("carrier.key").exists()); assert_eq!(app.page, Page::Onboard); assert_eq!(app.onboard.mode, OnboardMode::Start); } @@ -1883,7 +2195,7 @@ mod tests { app.handle(Message::SignupStartOver); assert!(app.master.is_none()); - assert!(!dir.join("carrier.key").exists()); + assert!(!app.dir.join("carrier.key").exists()); assert_eq!(app.page, Page::Onboard); assert!(app.signup.host.is_empty()); } diff --git a/src/demo.rs b/src/demo.rs index e21619d..dc038f4 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -1,7 +1,8 @@ //! Sample data for layout work, compiled into dev builds only. The dev //! build uses its own `carrier-demo` profile directory; seeding runs once -//! when it has no key yet, so read marks survive across refinement +//! when it has no accounts yet, so read marks survive across refinement //! sessions and the release profile's real account is never touched. +//! Two accounts are planted so the account switcher has something to show. use std::path::Path; @@ -22,15 +23,58 @@ fn identity(tag: u8) -> Identity { Identity::from_seed([tag; KEY_LEN]) } -pub fn seed(dir: &Path) { - if dir.join("carrier.key").exists() { +pub fn seed(root: &Path) { + if !crate::ops::list_accounts(root).is_empty() { return; } - if let Err(e) = plant(dir) { + let planted = (|| { + for id in ["demo", "demo2"] { + let dir = crate::ops::account_dir(root, id); + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + } + plant(&crate::ops::account_dir(root, "demo"))?; + plant_second(&crate::ops::account_dir(root, "demo2"))?; + crate::ops::save_active(root, "demo") + })(); + if let Err(e) = planted { eprintln!("demo seed failed: {e}"); } } +/// A sparse second account with its own master, so switching shows a +/// visibly different inbox. +fn plant_second(dir: &Path) -> Result<(), String> { + let master: [u8; KEY_LEN] = core::array::from_fn(|i| i as u8 + 101); + crate::ops::save_key(&dir.join("carrier.key"), &master)?; + let store = Store::open(&dir.join("carrier.db")).map_err(|e| e.to_string())?; + let me = Account::new(master, 0).map_err(|e| e.to_string())?; + let addr = Address::parse("demo2@carrier.local").map_err(|e| e.to_string())?; + store.set_account(&addr).map_err(|e| e.to_string())?; + store + .pin_server(&addr.host, addr.port, &identity(42).pk()) + .map_err(|e| e.to_string())?; + let sender = identity(11); + let uri = Address::parse("alice@example.org") + .unwrap() + .uri(&sender.pk()); + let text = build_frontmatter( + &[("Subject", "your second address works"), ("Reply-To", &uri)], + "This inbox belongs to the second demo account.", + ); + let envelope = seal(&sender, &me.me().pk(), text.as_bytes(), now() - 3600, true) + .map_err(|e| e.to_string())?; + store + .store_inbox( + &message_id(&envelope), + &envelope, + now() - 3600, + false, + false, + ) + .map_err(|e| e.to_string())?; + Ok(()) +} + fn plant(dir: &Path) -> Result<(), String> { let master = demo_master(); crate::ops::save_key(&dir.join("carrier.key"), &master)?; diff --git a/src/main.rs b/src/main.rs index 2284343..962bdc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ //! carrier — a minimalist smolmail client built on fumi-core, with a -//! Relm4 UI. State lives in the config directory: `carrier.key` (the -//! master, 0600), `carrier.db` (fumi-core's sealed store) and `read.txt` -//! (read marks, carrier's own sidecar). +//! Relm4 UI. Each account lives in its own `accounts//` folder under +//! the config directory: `carrier.key` (the master, 0600), `carrier.db` +//! (fumi-core's sealed store) and carrier's own sidecars (read marks, +//! drafts, settings). `active.txt` names the account the app opens. // No console window when the Windows release build is double-clicked. // Debug builds keep it for eprintln output. @@ -22,6 +23,10 @@ fn main() { eprintln!("cannot create {}: {e}", dir.display()); std::process::exit(1); } + if let Err(e) = ops::migrate_legacy(&dir) { + eprintln!("cannot move the account into {}: {e}", dir.display()); + std::process::exit(1); + } #[cfg(debug_assertions)] demo::seed(&dir); view::run(dir); diff --git a/src/ops.rs b/src/ops.rs index 6be3797..8d3444f 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -3,7 +3,7 @@ //! values these functions return. use std::collections::HashSet; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -138,8 +138,8 @@ pub struct Draft { pub at: i64, } -/// A fresh draft id: 8 random bytes as hex, unique within the sidecar. -pub fn draft_id() -> String { +/// A fresh id for a draft or an account folder: 8 random bytes as hex. +pub fn random_id() -> String { use rand_core::RngCore; let mut bytes = [0u8; 8]; rand_core::OsRng.fill_bytes(&mut bytes); @@ -399,19 +399,13 @@ pub fn delete_key(path: &Path) -> Result<(), String> { } } -/// Removes every file carrier owns: the store, the key and both sidecars. +/// Removes every file an account owns: the store, the key and the sidecars. /// A missing file is fine — reset is idempotent — but a file that cannot /// be removed is reported, so a reset that did not take is never silent. /// The caller must close the store first: Windows refuses to remove a /// file a live connection holds. pub fn factory_reset(dir: &Path) -> Result<(), String> { - for name in [ - "carrier.db", - "carrier.key", - "read.txt", - "drafts.txt", - "settings.txt", - ] { + for name in ACCOUNT_FILES { let path = dir.join(name); if let Err(e) = std::fs::remove_file(&path) { if e.kind() != std::io::ErrorKind::NotFound { @@ -425,6 +419,113 @@ pub fn factory_reset(dir: &Path) -> Result<(), String> { Ok(()) } +/// The files one account owns. The WAL pair is listed so a migration never +/// strands commits a crash left uncheckpointed. +const ACCOUNT_FILES: [&str; 7] = [ + "carrier.db", + "carrier.db-wal", + "carrier.db-shm", + "carrier.key", + "read.txt", + "drafts.txt", + "settings.txt", +]; + +pub fn accounts_dir(root: &Path) -> PathBuf { + root.join("accounts") +} + +pub fn account_dir(root: &Path, id: &str) -> PathBuf { + accounts_dir(root).join(id) +} + +/// Moves a single-account config root into `accounts//` and makes it +/// the active account. Runs before anything opens the files; a root that +/// already has `accounts/`, or holds none of the files, is left alone. +pub fn migrate_legacy(root: &Path) -> Result<(), String> { + if accounts_dir(root).exists() { + return Ok(()); + } + let present: Vec<&str> = ACCOUNT_FILES + .into_iter() + .filter(|name| root.join(name).exists()) + .collect(); + if present.is_empty() { + return Ok(()); + } + let id = random_id(); + let dir = account_dir(root, &id); + std::fs::create_dir_all(&dir) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(&dir, &e))))?; + for name in present { + let from = root.join(name); + std::fs::rename(&from, dir.join(name)) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(&from, &e))))?; + } + save_active(root, &id) +} + +/// Account folder names, sorted so the switcher's order is stable. +pub fn list_accounts(root: &Path) -> Vec { + let mut ids: Vec = std::fs::read_dir(accounts_dir(root)) + .map(|entries| { + entries + .flatten() + .filter(|entry| entry.path().is_dir()) + .filter_map(|entry| entry.file_name().into_string().ok()) + .collect() + }) + .unwrap_or_default(); + ids.sort(); + ids +} + +/// The active account's id, if `active.txt` names a folder that exists. +pub fn load_active(root: &Path) -> Option { + let id = std::fs::read_to_string(root.join("active.txt")).ok()?; + let id = id.trim().to_string(); + (!id.is_empty() && account_dir(root, &id).is_dir()).then_some(id) +} + +pub fn save_active(root: &Path, id: &str) -> Result<(), String> { + let path = root.join("active.txt"); + std::fs::write(&path, format!("{id}\n")) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(&path, &e)))) +} + +/// Creates an empty account folder and returns its id. +pub fn new_account(root: &Path) -> Result { + let id = random_id(); + let dir = account_dir(root, &id); + std::fs::create_dir_all(&dir) + .map_err(|e| i18n::t_args("file-error", Some(&file_error_args(&dir, &e))))?; + Ok(id) +} + +/// Deletes an account's files, then its folder. Same contract as +/// `factory_reset`: the caller must close the account's store first. +pub fn remove_account(root: &Path, id: &str) -> Result<(), String> { + let dir = account_dir(root, id); + factory_reset(&dir)?; + match std::fs::remove_dir(&dir) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(i18n::t_args("file-error", Some(&file_error_args(&dir, &e)))), + } +} + +/// The address an account folder is registered under, read from its own +/// store; None for a folder that isn't registered or can't be opened. +pub fn account_label(dir: &Path) -> Option { + // Opening a missing database would create one. + let path = dir.join("carrier.db"); + if !path.exists() { + return None; + } + let store = Store::open(&path).ok()?; + store.account().ok().flatten().map(|addr| addr.short()) +} + pub fn account(store: &Store, master: &[u8; KEY_LEN]) -> Result { let index = store.rotations().map_err(explain)?; account::Account::new(*master, index).map_err(explain) @@ -966,13 +1067,55 @@ mod tests { assert_eq!(load_settings(&path).timeout, 30); } + #[test] + fn a_legacy_root_migrates_into_an_account_folder_once() { + let root = tempfile::tempdir().unwrap(); + let root = root.path(); + // A fresh root has nothing to move. + migrate_legacy(root).unwrap(); + assert!(list_accounts(root).is_empty()); + + for name in ["carrier.key", "carrier.db", "carrier.db-wal", "read.txt"] { + std::fs::write(root.join(name), name).unwrap(); + } + migrate_legacy(root).unwrap(); + let ids = list_accounts(root); + assert_eq!(ids.len(), 1); + assert_eq!(load_active(root).as_ref(), Some(&ids[0])); + let dir = account_dir(root, &ids[0]); + for name in ["carrier.key", "carrier.db", "carrier.db-wal", "read.txt"] { + assert!(!root.join(name).exists(), "{name} stayed in the root"); + assert_eq!(std::fs::read_to_string(dir.join(name)).unwrap(), name); + } + + // Once `accounts/` exists, stray root files are left alone. + std::fs::write(root.join("carrier.key"), "stray").unwrap(); + migrate_legacy(root).unwrap(); + assert_eq!(list_accounts(root).len(), 1); + assert!(root.join("carrier.key").exists()); + } + + #[test] + fn the_active_account_must_name_an_existing_folder() { + let root = tempfile::tempdir().unwrap(); + let root = root.path(); + save_active(root, "gone").unwrap(); + assert!(load_active(root).is_none()); + let id = new_account(root).unwrap(); + save_active(root, &id).unwrap(); + assert_eq!(load_active(root), Some(id.clone())); + remove_account(root, &id).unwrap(); + assert!(load_active(root).is_none()); + assert!(list_accounts(root).is_empty()); + } + #[test] fn drafts_round_trip_through_the_sidecar() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("drafts.txt"); let drafts = vec![ Draft { - id: draft_id(), + id: random_id(), to: "smol://alice@example.org/KEY".into(), subject: "line breaks\nand \\backslashes\r".into(), body: "a draft\n\nwith paragraphs\nand a \\ literal".into(), @@ -987,7 +1130,7 @@ mod tests { at: now(), }, Draft { - id: draft_id(), + id: random_id(), to: "bob@example.org".into(), subject: "no reply".into(), body: String::new(), @@ -1287,7 +1430,8 @@ mod tests { fn demo_seed_carries_a_mail_with_custom_fields() { let dir = tempfile::tempdir().unwrap(); crate::demo::seed(dir.path()); - let store = Store::open(&dir.path().join("carrier.db")).unwrap(); + let account = account_dir(dir.path(), "demo"); + let store = Store::open(&account.join("carrier.db")).unwrap(); // The demo master is fixed bytes in demo.rs. let master: [u8; KEY_LEN] = core::array::from_fn(|i| i as u8 + 1); let empty = HashSet::new(); diff --git a/src/view.rs b/src/view.rs index b14ba34..8ed9952 100644 --- a/src/view.rs +++ b/src/view.rs @@ -21,8 +21,8 @@ use relm4::ComponentSender; use relm4::{Component, ComponentParts}; use crate::app::{ - Carrier, Dialog, DialogKind, Message, OnboardMode, OpResult, Page, PendingOp, SignupMode, - SignupStep, + AccountEntry, Carrier, Dialog, DialogKind, Message, OnboardMode, OpResult, Page, PendingOp, + SignupMode, SignupStep, }; use crate::i18n; use crate::ops::{self, Contact, Folder, Row}; @@ -178,6 +178,32 @@ fn contact_state(contact: &Contact) -> String { i18n::t(id) } +/// An account's address, or the not-registered label while it has none. +fn account_title(entry: &AccountEntry) -> String { + entry + .label + .clone() + .unwrap_or_else(|| i18n::t("account-not-registered-value")) +} + +/// One switcher row, named by account id; the active one carries a check. +fn account_row(entry: &AccountEntry, active: bool) -> gtk::ListBoxRow { + let line = gtk::Box::new(Orientation::Horizontal, 6); + line.set_margin_top(6); + line.set_margin_bottom(6); + let title = label(&account_title(entry), "mono"); + title.set_hexpand(true); + title.set_ellipsize(gtk::pango::EllipsizeMode::End); + line.append(&title); + let check = gtk::Image::from_icon_name("object-select-symbolic"); + check.set_opacity(if active { 1.0 } else { 0.0 }); + line.append(&check); + let row = gtk::ListBoxRow::new(); + row.set_widget_name(&entry.id); + row.set_child(Some(&line)); + row +} + #[derive(Debug, Clone)] pub struct CarrierWidgets { /// `AlertDialog::present` needs a parent, and only the window is one. @@ -225,6 +251,9 @@ pub struct CarrierWidgets { // sidebar bottom: own identity and the contact list sidebar_own: gtk::Label, sidebar_contacts: gtk::ListBox, + /// The account switcher beside the identity card, and its list. + accounts_menu: gtk::MenuButton, + accounts_list: gtk::ListBox, // the QR-only share popover on the detail view's header detail_share: gtk::MenuButton, @@ -245,6 +274,9 @@ pub struct CarrierWidgets { // onboarding onboard_seed_label: gtk::Label, onboard_seed_entry: adw::EntryRow, + onboard_reuse: gtk::Box, + onboard_reuse_list: gtk::ListBox, + onboard_cancel: gtk::Button, // signup signup_key_button: gtk::Button, @@ -392,10 +424,37 @@ impl CarrierWidgets { own_text.append(&sidebar_own); let own_row = gtk::Button::new(); own_row.add_css_class("card"); - own_row.set_halign(Align::Fill); - own_row.set_margin_top(6); + own_row.set_hexpand(true); own_row.set_child(Some(&own_text)); - sidebar_bottom.append(&own_row); + + // The account switcher sits beside the card: one list of accounts, + // the active one checked, and a way to add another. + let accounts_list = gtk::ListBox::new(); + accounts_list.add_css_class("navigation-sidebar"); + accounts_list.set_selection_mode(gtk::SelectionMode::None); + let add_account = icon_label_button( + "list-add-symbolic", + &i18n::t("accounts-add-button"), + "quiet", + ); + let accounts_box = gtk::Box::new(Orientation::Vertical, 6); + accounts_box.set_width_request(260); + accounts_box.append(&accounts_list); + accounts_box.append(&add_account); + let accounts_popover = gtk::Popover::new(); + accounts_popover.set_child(Some(&accounts_box)); + let accounts_menu = gtk::MenuButton::new(); + accounts_menu.set_icon_name("system-switch-user-symbolic"); + accounts_menu.add_css_class("flat"); + accounts_menu.add_css_class("circular"); + accounts_menu.set_valign(Align::Center); + accounts_menu.set_tooltip_text(Some(&i18n::t("accounts-menu-tooltip"))); + accounts_menu.set_popover(Some(&accounts_popover)); + let own_line = gtk::Box::new(Orientation::Horizontal, 6); + own_line.set_margin_top(6); + own_line.append(&own_row); + own_line.append(&accounts_menu); + sidebar_bottom.append(&own_line); // The list's header carries its one action: importing a // smol:// address, in a popover off a plain add button. @@ -937,6 +996,19 @@ impl CarrierWidgets { let restore_button = button(&i18n::t("onboard-restore-button"), "pill-quiet"); onboard_start.append(&create_button); onboard_start.append(&restore_button); + // Shown only while other accounts hold a secret: one row each. + let onboard_reuse = gtk::Box::new(Orientation::Vertical, 6); + onboard_reuse.set_margin_top(12); + onboard_reuse.append(&label(&i18n::t("onboard-reuse-title"), "heading")); + let reuse_description = label(&i18n::t("onboard-reuse-description"), "dim"); + reuse_description.set_wrap(true); + onboard_reuse.append(&reuse_description); + let onboard_reuse_list = boxed_list(); + onboard_reuse.append(&onboard_reuse_list); + onboard_start.append(&onboard_reuse); + let onboard_cancel = button(&i18n::t("accounts-add-cancel-button"), "pill-quiet"); + onboard_cancel.set_margin_top(12); + onboard_start.append(&onboard_cancel); stack.add_named(¢er(&onboard_start), Some("onboard")); let onboard_created = gtk::Box::new(Orientation::Vertical, 12); @@ -1196,6 +1268,17 @@ impl CarrierWidgets { }); own_row.connect_clicked(input(Message::OwnSelected)); + let switch_sender = sender.clone(); + let switch_popover = accounts_popover.clone(); + accounts_list.connect_row_activated(move |_, row| { + switch_popover.popdown(); + switch_sender.input(Message::AccountSwitch(row.widget_name().to_string())); + }); + let add_sender = sender.clone(); + add_account.connect_clicked(move |_| { + accounts_popover.popdown(); + add_sender.input(Message::AccountAdd); + }); import_button.connect_clicked(input(Message::ContactImportPressed)); let import_sender = sender.clone(); contact_import.connect_changed(move |entry| { @@ -1269,6 +1352,11 @@ impl CarrierWidgets { create_button.connect_clicked(input(Message::OnboardCreate)); restore_button.connect_clicked(input(Message::OnboardRestore)); + onboard_cancel.connect_clicked(input(Message::AccountAddCancelled)); + let reuse_sender = sender.clone(); + onboard_reuse_list.connect_row_activated(move |_, row| { + reuse_sender.input(Message::OnboardReuse(row.widget_name().to_string())); + }); restore_back.connect_clicked(input(Message::OnboardBack)); created_back.connect_clicked(input(Message::OnboardBack)); saved_button.connect_clicked(input(Message::OnboardSeedSaved)); @@ -1348,6 +1436,8 @@ impl CarrierWidgets { compose_pin_button, sidebar_own, sidebar_contacts: contacts_list, + accounts_menu, + accounts_list, detail_share, detail_copy, detail_qr, @@ -1362,6 +1452,9 @@ impl CarrierWidgets { timeout_row, onboard_seed_label, onboard_seed_entry, + onboard_reuse, + onboard_reuse_list, + onboard_cancel, signup_key_button, signup_key_card, signup_key_value, @@ -2055,6 +2148,30 @@ fn render(model: &Carrier, w: &mut CarrierWidgets, sender: &ComponentSender Date: Wed, 14 Oct 2026 17:40:02 +0300 Subject: [PATCH 35/37] chore: bump version to 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- data/com.randogoth.carrier.metainfo.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63cf783..c066ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ [[package]] name = "carrier" -version = "0.2.0" +version = "0.3.0" dependencies = [ "fluent-bundle", "fumi-core", diff --git a/Cargo.toml b/Cargo.toml index fdebc4e..ba38e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "carrier" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "A minimalist smolmail client built on fumi-core, with a Relm4 UI" license = "Apache-2.0" diff --git a/data/com.randogoth.carrier.metainfo.xml b/data/com.randogoth.carrier.metainfo.xml index 868afaa..80ce224 100644 --- a/data/com.randogoth.carrier.metainfo.xml +++ b/data/com.randogoth.carrier.metainfo.xml @@ -14,6 +14,7 @@ com.randogoth.carrier.desktop + From d9798a18f1b1586c9e791ce7ae2ef1817d7b6c72 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 14 Oct 2026 22:08:55 +0300 Subject: [PATCH 36/37] fix: trim the body before signing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compose buffer was passed through verbatim, so whatever the author last pressed ended up in the payload — usually a trailing newline. That is not content, but it becomes content the moment it is signed, and nothing downstream can remove it without invalidating the signature. smolboid refuses any post whose text carries a line break, because a published record is one line of a feed, so every post sent from carrier was rejected with "contains TAB, LF, CR or U+2028". Trimming has to happen here, before the seal, rather than at the receiver: stripping is not invertible, so a receiver that trimmed could no longer rebuild the bytes the author signed. --- src/ops.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ops.rs b/src/ops.rs index 8d3444f..103bce0 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -716,7 +716,14 @@ pub fn send( .collect(); let draft = client::SendDraft { address: &addr, - text: body.to_string(), + // Trimmed at the send site, before the payload is signed, so the + // signature covers exactly the text the author meant. A compose buffer + // ends with whatever the author last pressed, and a trailing newline + // is not content — but it is content once it has been signed, and + // nothing downstream can remove it without invalidating the + // signature. smolboid, for one, refuses a post whose text carries a + // line break, because a published record is one line of a feed. + text: body.trim().to_string(), subject, reply_to, headers: &owned, From c5ad8d44e9ea77bbcebd18f95b13b34b0f1f778b Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 15 Oct 2026 09:22:50 +0300 Subject: [PATCH 37/37] chore: release 0.3.1 Trims the body before signing it, so a trailing newline from the compose buffer no longer ends up in the payload. A published record can't contain a line break, so every post sent from here to a twtxt feed was refused. Builds against fumi-core 0.1.1, which hands an accept token to a contact once instead of attaching it to every message. --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- data/com.randogoth.carrier.metainfo.xml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c066ee3..afbbcc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ [[package]] name = "carrier" -version = "0.3.0" +version = "0.3.1" dependencies = [ "fluent-bundle", "fumi-core", @@ -430,7 +430,7 @@ dependencies = [ [[package]] name = "fumi-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chacha20poly1305", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index ba38e7e..b124695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "carrier" -version = "0.3.0" +version = "0.3.1" edition = "2021" description = "A minimalist smolmail client built on fumi-core, with a Relm4 UI" license = "Apache-2.0" diff --git a/data/com.randogoth.carrier.metainfo.xml b/data/com.randogoth.carrier.metainfo.xml index 80ce224..e8a4fa9 100644 --- a/data/com.randogoth.carrier.metainfo.xml +++ b/data/com.randogoth.carrier.metainfo.xml @@ -14,6 +14,7 @@ com.randogoth.carrier.desktop +