diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..3d73f37 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,162 @@ +name: Preview Release + +# Builds the `preview` channel on every push to main: a co-installable build +# that tracks main, published to a rolling `preview` prerelease and the +# `annot-preview` Homebrew formula. Stable releases are untouched (release.yml). + +on: + push: + branches: + - main + workflow_dispatch: + +# A push supersedes any in-flight preview build, so the tap always reflects +# the tip of main. +concurrency: + group: preview-release + cancel-in-progress: true + +permissions: + contents: write + +jobs: + build-preview: + runs-on: macos-26 # Apple Silicon runner (Tahoe) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history + tags, for the git-describe version + + - name: Compute preview version + id: version + run: | + SHA=$(git rev-parse --short HEAD) + if LAST_TAG=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null); then + BASE=${LAST_TAG#v} + COUNT=$(git rev-list --count "${LAST_TAG}..HEAD") + else + BASE="0.0.0" + COUNT=$(git rev-list --count HEAD) + fi + # Monotonic on COUNT (drives `brew upgrade`); SHA for traceability. + VERSION="${BASE}-preview.${COUNT}.g${SHA}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "Preview version: $VERSION" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install Node dependencies + run: pnpm install + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Import Apple certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + # Create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import certificate + echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 + security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + - name: Build Tauri app (preview channel) + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Baked into the binary at compile time (see src-tauri/src/channel.rs). + ANNOT_CHANNEL: preview + ANNOT_BUILD_SHA: g${{ steps.version.outputs.sha }} + run: pnpm tauri build --config src-tauri/tauri.preview.conf.json + + - name: Package artifact + run: | + cd src-tauri/target/release/bundle/macos + mkdir annot-preview + cp -R "annot (preview).app" annot-preview/ + tar -czf annot-preview-darwin-arm64.tar.gz annot-preview + mv annot-preview-darwin-arm64.tar.gz "$GITHUB_WORKSPACE/" + + - name: Publish rolling preview release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Delete + recreate so the `preview` tag and asset always point at + # the tip of main (Neovim's nightly pattern). + gh release delete preview --yes --cleanup-tag || true + gh release create preview \ + --prerelease \ + --target "$GITHUB_SHA" \ + --title "Preview build (${{ steps.version.outputs.version }})" \ + --notes "Rolling build from \`main\`. Install with \`brew install denolehov/tap/annot-preview\`." \ + annot-preview-darwin-arm64.tar.gz + + - name: Update annot-preview formula in tap + env: + TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + SHA256=$(shasum -a 256 annot-preview-darwin-arm64.tar.gz | cut -d' ' -f1) + git clone "https://x-access-token:${TAP_TOKEN}@github.com/denolehov/homebrew-tap.git" tap + cat > tap/Formula/annot-preview.rb < 1 + else Path(tempfile.gettempdir()) / "annot-preview-icon-source.png" +) + +HUE_SHIFT_DEGREES = 120 # recolors green->blue, gold->magenta, purple->green + +img = Image.open(SRC).convert("RGBA") +r, g, b, alpha = img.split() +hue, sat, val = Image.merge("RGB", (r, g, b)).convert("HSV").split() +shift = round(HUE_SHIFT_DEGREES / 360 * 256) +hue = hue.point(lambda x: (x + shift) % 256) +recolored = Image.merge("HSV", (hue, sat, val)).convert("RGB") +out = Image.merge("RGBA", (*recolored.split(), alpha)) + +DST.parent.mkdir(parents=True, exist_ok=True) +out.save(DST) +print(f"wrote {DST}") diff --git a/src-tauri/icons-preview/128x128.png b/src-tauri/icons-preview/128x128.png new file mode 100644 index 0000000..369e5d6 Binary files /dev/null and b/src-tauri/icons-preview/128x128.png differ diff --git a/src-tauri/icons-preview/128x128@2x.png b/src-tauri/icons-preview/128x128@2x.png new file mode 100644 index 0000000..2dfccf4 Binary files /dev/null and b/src-tauri/icons-preview/128x128@2x.png differ diff --git a/src-tauri/icons-preview/32x32.png b/src-tauri/icons-preview/32x32.png new file mode 100644 index 0000000..44bde92 Binary files /dev/null and b/src-tauri/icons-preview/32x32.png differ diff --git a/src-tauri/icons-preview/64x64.png b/src-tauri/icons-preview/64x64.png new file mode 100644 index 0000000..0ae3ab0 Binary files /dev/null and b/src-tauri/icons-preview/64x64.png differ diff --git a/src-tauri/icons-preview/Square107x107Logo.png b/src-tauri/icons-preview/Square107x107Logo.png new file mode 100644 index 0000000..60d472c Binary files /dev/null and b/src-tauri/icons-preview/Square107x107Logo.png differ diff --git a/src-tauri/icons-preview/Square142x142Logo.png b/src-tauri/icons-preview/Square142x142Logo.png new file mode 100644 index 0000000..438c0e3 Binary files /dev/null and b/src-tauri/icons-preview/Square142x142Logo.png differ diff --git a/src-tauri/icons-preview/Square150x150Logo.png b/src-tauri/icons-preview/Square150x150Logo.png new file mode 100644 index 0000000..65b4e5e Binary files /dev/null and b/src-tauri/icons-preview/Square150x150Logo.png differ diff --git a/src-tauri/icons-preview/Square284x284Logo.png b/src-tauri/icons-preview/Square284x284Logo.png new file mode 100644 index 0000000..c29eef5 Binary files /dev/null and b/src-tauri/icons-preview/Square284x284Logo.png differ diff --git a/src-tauri/icons-preview/Square30x30Logo.png b/src-tauri/icons-preview/Square30x30Logo.png new file mode 100644 index 0000000..5daa85b Binary files /dev/null and b/src-tauri/icons-preview/Square30x30Logo.png differ diff --git a/src-tauri/icons-preview/Square310x310Logo.png b/src-tauri/icons-preview/Square310x310Logo.png new file mode 100644 index 0000000..0bf0477 Binary files /dev/null and b/src-tauri/icons-preview/Square310x310Logo.png differ diff --git a/src-tauri/icons-preview/Square44x44Logo.png b/src-tauri/icons-preview/Square44x44Logo.png new file mode 100644 index 0000000..c3d4f76 Binary files /dev/null and b/src-tauri/icons-preview/Square44x44Logo.png differ diff --git a/src-tauri/icons-preview/Square71x71Logo.png b/src-tauri/icons-preview/Square71x71Logo.png new file mode 100644 index 0000000..165de3a Binary files /dev/null and b/src-tauri/icons-preview/Square71x71Logo.png differ diff --git a/src-tauri/icons-preview/Square89x89Logo.png b/src-tauri/icons-preview/Square89x89Logo.png new file mode 100644 index 0000000..14bbc25 Binary files /dev/null and b/src-tauri/icons-preview/Square89x89Logo.png differ diff --git a/src-tauri/icons-preview/StoreLogo.png b/src-tauri/icons-preview/StoreLogo.png new file mode 100644 index 0000000..34d676d Binary files /dev/null and b/src-tauri/icons-preview/StoreLogo.png differ diff --git a/src-tauri/icons-preview/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons-preview/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons-preview/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..8ecd3b1 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..bec8502 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e25a4f Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..2166779 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..234ab6a Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..3dabe1b Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a1d2219 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..48f623a Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..7565fdb Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..c757931 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1c5b0ca Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..9eaff3e Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..25830df Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..41f917d Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1c28e54 Binary files /dev/null and b/src-tauri/icons-preview/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons-preview/android/values/ic_launcher_background.xml b/src-tauri/icons-preview/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons-preview/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons-preview/icon.icns b/src-tauri/icons-preview/icon.icns new file mode 100644 index 0000000..175f72e Binary files /dev/null and b/src-tauri/icons-preview/icon.icns differ diff --git a/src-tauri/icons-preview/icon.ico b/src-tauri/icons-preview/icon.ico new file mode 100644 index 0000000..8926cb4 Binary files /dev/null and b/src-tauri/icons-preview/icon.ico differ diff --git a/src-tauri/icons-preview/icon.png b/src-tauri/icons-preview/icon.png new file mode 100644 index 0000000..bdc7c1d Binary files /dev/null and b/src-tauri/icons-preview/icon.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-20x20@1x.png b/src-tauri/icons-preview/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..48d7c42 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons-preview/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..c73a269 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-20x20@2x.png b/src-tauri/icons-preview/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..c73a269 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-20x20@3x.png b/src-tauri/icons-preview/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..d2fd4c9 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-29x29@1x.png b/src-tauri/icons-preview/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..fcec2c5 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons-preview/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..d30c65e Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-29x29@2x.png b/src-tauri/icons-preview/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..d30c65e Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-29x29@3x.png b/src-tauri/icons-preview/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..953001f Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-40x40@1x.png b/src-tauri/icons-preview/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..c73a269 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons-preview/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..ba69e7b Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-40x40@2x.png b/src-tauri/icons-preview/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..ba69e7b Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-40x40@3x.png b/src-tauri/icons-preview/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..f8db672 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-512@2x.png b/src-tauri/icons-preview/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..f89e49b Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-60x60@2x.png b/src-tauri/icons-preview/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..f8db672 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-60x60@3x.png b/src-tauri/icons-preview/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..2dadb04 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-76x76@1x.png b/src-tauri/icons-preview/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..2ac6b3b Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-76x76@2x.png b/src-tauri/icons-preview/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..abc5d11 Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons-preview/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons-preview/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..163a5fd Binary files /dev/null and b/src-tauri/icons-preview/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/channel.rs b/src-tauri/src/channel.rs new file mode 100644 index 0000000..792dc87 --- /dev/null +++ b/src-tauri/src/channel.rs @@ -0,0 +1,124 @@ +//! Release channel identity. +//! +//! annot ships two channels: `stable` (tagged releases) and `preview` (builds +//! that track `main`). The channel is baked in at compile time from the +//! `ANNOT_CHANNEL` and `ANNOT_BUILD_SHA` environment variables, which CI sets +//! for preview builds. A normal local or release build leaves them unset and +//! is therefore `stable` — behaviour is byte-identical to a build that never +//! had this module. + +use std::sync::OnceLock; + +use serde::Serialize; + +/// Release channel this binary was built for. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Channel { + /// Tagged release build. The default for any build without CI overrides. + #[default] + Stable, + /// Build tracking `main`; carries the short commit SHA it was built from. + Preview(String), +} + +/// Raw channel name baked in at compile time. `"stable"` unless CI overrides it. +const CHANNEL: &str = match option_env!("ANNOT_CHANNEL") { + Some(c) => c, + None => "stable", +}; + +/// Short commit SHA this binary was built from. `"dev"` for local builds. +const BUILD_SHA: &str = match option_env!("ANNOT_BUILD_SHA") { + Some(s) => s, + None => "dev", +}; + +/// The channel this binary was built for. Resolved once from compile-time env. +pub fn current() -> &'static Channel { + static CURRENT: OnceLock = OnceLock::new(); + CURRENT.get_or_init(|| match CHANNEL { + "preview" => Channel::Preview(BUILD_SHA.to_string()), + _ => Channel::Stable, + }) +} + +impl Channel { + /// Whether this is a non-stable (preview) build. + pub fn is_preview(&self) -> bool { + matches!(self, Channel::Preview(_)) + } + + /// Human-facing application name. Preview builds carry a visible suffix so + /// stable and preview windows are distinguishable at a glance. + pub fn display_name(&self) -> &'static str { + match self { + Channel::Stable => "annot", + Channel::Preview(_) => "annot (preview)", + } + } + + /// Config subdirectory name under the OS config dir. Preview gets its own + /// directory, isolated from stable's tags, exit modes, and bookmarks. + pub fn config_subdir(&self) -> &'static str { + match self { + Channel::Stable => "annot", + Channel::Preview(_) => "annot-preview", + } + } + + /// Build marker for non-stable builds, e.g. `"preview · g1a2b3c"`. + /// `None` for stable, so stable output stays unchanged. + pub fn marker(&self) -> Option { + match self { + Channel::Stable => None, + Channel::Preview(sha) => Some(format!("preview · {sha}")), + } + } +} + +/// Full version string: the crate version, plus a channel marker for +/// non-stable builds. Used by both `annot version` and clap's `--version` +/// (which requires a `&'static str`), so the result is computed once and cached. +pub fn version_string() -> &'static str { + static CACHE: OnceLock = OnceLock::new(); + CACHE + .get_or_init(|| match current().marker() { + Some(marker) => format!("{} ({})", env!("CARGO_PKG_VERSION"), marker), + None => env!("CARGO_PKG_VERSION").to_string(), + }) + .as_str() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stable_is_the_default() { + // The test binary compiles without ANNOT_CHANNEL set, so it is stable. + assert_eq!(*current(), Channel::Stable); + assert!(!current().is_preview()); + assert_eq!(current().display_name(), "annot"); + assert_eq!(current().config_subdir(), "annot"); + assert_eq!(current().marker(), None); + assert_eq!(version_string(), env!("CARGO_PKG_VERSION")); + } + + #[test] + fn channels_never_share_a_config_dir() { + // The isolation invariant, independent of which channel compiled this. + assert_ne!( + Channel::Stable.config_subdir(), + Channel::Preview(String::new()).config_subdir() + ); + } + + #[test] + fn preview_carries_sha_in_marker_and_name() { + let ch = Channel::Preview("g1a2b3c".to_string()); + assert!(ch.is_preview()); + assert_eq!(ch.marker(), Some("preview · g1a2b3c".to_string())); + assert_eq!(ch.display_name(), "annot (preview)"); + } +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index f820ee7..376ba76 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -158,9 +158,11 @@ fn save_merged( Ok(()) } -/// Returns the config directory path: ~/.config/annot/ on Unix, %APPDATA%\annot\ on Windows. +/// Returns the config directory path, e.g. `~/.config/annot/` on Unix or +/// `%APPDATA%\annot\` on Windows. Preview builds resolve to an `annot-preview` +/// subdirectory instead, keeping their config isolated from stable's. pub fn config_dir() -> Option { - dirs::config_dir().map(|p| p.join("annot")) + dirs::config_dir().map(|p| p.join(crate::channel::current().config_subdir())) } /// Ensures the config directory exists. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 203f60a..4bef4a1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ use parking_lot::Mutex; use tauri::WebviewWindowBuilder; +pub mod channel; pub mod commands; pub mod config; pub mod diff; @@ -140,7 +141,7 @@ pub fn run(state: AppState, context: tauri::Context, json_output: bool) { "main", tauri::WebviewUrl::App("index.html".into()), ) - .title("annot") + .title(channel::current().display_name()) .inner_size(1000.0, 700.0) .visible(false); // Will be shown after content loads #[cfg(target_os = "macos")] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 26740bd..e453e79 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -20,7 +20,7 @@ Examples: #[derive(Parser)] #[command(name = "annot")] -#[command(version)] +#[command(version = annot_lib::channel::version_string())] #[command(about = "Ephemeral annotation tool for AI workflows")] #[command(long_about = EXAMPLES)] struct Cli { @@ -103,7 +103,7 @@ fn main() { // Handle version subcommand (doesn't need Tauri) if let Some(Command::Version) = cli.command { - println!("annot {}", env!("CARGO_PKG_VERSION")); + println!("annot {}", annot_lib::channel::version_string()); return; } diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index d1de04b..b15ef1c 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -414,7 +414,7 @@ fn run_session_with_state( let builder = { let b = WebviewWindowBuilder::new(app_handle, &window_label, tauri::WebviewUrl::App("index.html".into())) - .title("annot") + .title(crate::channel::current().display_name()) .inner_size(1000.0, 700.0) .visible(false); // Will be shown after content loads #[cfg(target_os = "macos")] diff --git a/src-tauri/src/output/mod.rs b/src-tauri/src/output/mod.rs index f467cd8..2271d06 100644 --- a/src-tauri/src/output/mod.rs +++ b/src-tauri/src/output/mod.rs @@ -13,6 +13,7 @@ mod snapshot_tests; use std::collections::{BTreeMap, HashMap}; +use crate::channel::Channel; use crate::lang; use crate::mcp::tools::SessionImage; use crate::portal::LoadedPortal; @@ -56,11 +57,14 @@ pub struct FormatMetadata { pub general_comment_count: usize, pub terraform_count: usize, pub bookmark_count: usize, + /// Release channel this build belongs to. `Stable` on release builds, so + /// stable output is unaffected; `Preview(sha)` carries the build's SHA. + pub channel: Channel, } /// Serialize a FormatResult as JSON for `--json` CLI output. pub fn format_json(result: &FormatResult) -> String { - let json = serde_json::json!({ + let mut json = serde_json::json!({ "text": result.text, "images": result.images.iter().map(|img| serde_json::json!({ "figure": img.figure, @@ -74,6 +78,11 @@ pub fn format_json(result: &FormatResult) -> String { "terraform_count": result.metadata.terraform_count, "bookmark_count": result.metadata.bookmark_count, }); + // Only non-stable builds carry a `channel` key, so stable JSON is unchanged. + if result.metadata.channel.is_preview() { + json["channel"] = serde_json::to_value(&result.metadata.channel) + .expect("Channel JSON serialization should not fail"); + } serde_json::to_string(&json).expect("FormatResult JSON serialization should not fail") } @@ -398,6 +407,13 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { let builder_mode = calculate_builder_mode(content, max_line); let mut out = OutputBuilder::new(builder_mode); + // Non-stable builds announce themselves so feedback is traceable to a + // specific build. Stable adds nothing here, keeping its output unchanged. + if let Some(marker) = crate::channel::current().marker() { + out.raw_line(&format!("[annot {marker}]")); + out.blank_line(); + } + // TAGS block (if any tags are used) let unique_tags = collect_unique_tags(review); if !unique_tags.is_empty() { @@ -601,6 +617,7 @@ pub fn format_output(review: &Review, mode: OutputMode) -> FormatResult { general_comment_count, terraform_count, bookmark_count, + channel: crate::channel::current().clone(), }, } } diff --git a/src-tauri/tauri.preview.conf.json b/src-tauri/tauri.preview.conf.json new file mode 100644 index 0000000..b43cd39 --- /dev/null +++ b/src-tauri/tauri.preview.conf.json @@ -0,0 +1,14 @@ +{ + "productName": "annot (preview)", + "identifier": "com.denolehov.annot.preview", + "mainBinaryName": "annot-preview", + "bundle": { + "icon": [ + "icons-preview/32x32.png", + "icons-preview/128x128.png", + "icons-preview/128x128@2x.png", + "icons-preview/icon.icns", + "icons-preview/icon.ico" + ] + } +}