From 20b2d87173870d939002efe84fddff2e944eabd6 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Fri, 27 Feb 2026 13:18:14 -0700 Subject: [PATCH 1/2] docs: Add dedicated theme document. - Update theme document reference in README. --- README.md | 30 ++------------------ THEMES.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 THEMES.md diff --git a/README.md b/README.md index 17ff976..73761f4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Pomotroid provides many themes. It's also theme-able, allowing you to customize ![Screenshots of Pomotroid using various themes](./.github/images/pomotroid_themes-preview--914x219.png) -Visit the [theme documentation](./docs/themes/themes.md) to view the full list of official themes and for instruction on creating your own. +See [THEMES.md](./THEMES.md) for the full theme list and instructions on creating your own. ## Install @@ -85,33 +85,7 @@ appget install pomotroid ## Custom Themes -Pomotroid supports custom themes defined as JSON files placed in the app's configuration directory: - -- **Linux**: `~/.config/pomotroid/themes/` -- **macOS**: `~/Library/Application Support/pomotroid/themes/` -- **Windows**: `%APPDATA%\pomotroid\themes\` - -Custom themes are hot-reloaded automatically — no restart required. - -Each theme file must follow this format: - -```json -{ - "name": "My Theme", - "colors": { - "--color-long-round": "#c75000", - "--color-short-round": "#417505", - "--color-focus-round": "#b01c2e", - "--color-background": "#2f384b", - "--color-background-light": "#3e4a5d", - "--color-foreground": "#d7e1f4", - "--color-foreground-darker": "#a3aec4", - "--color-accent": "#ff6347", - "--color-accent-extra": "#f0c050", - "--color-gradient": "#1e2430" - } -} -``` +Pomotroid supports user-created themes with automatic hot-reload — no restart required. See [THEMES.md](./THEMES.md) for directory paths, the full color reference, and a step-by-step guide. ## WebSocket API diff --git a/THEMES.md b/THEMES.md new file mode 100644 index 0000000..f988617 --- /dev/null +++ b/THEMES.md @@ -0,0 +1,85 @@ +# Pomotroid Themes + +Pomotroid ships with 18 built-in themes and supports an unlimited number of user-created custom themes. Custom themes are hot-reloaded — no restart required. + +## Built-in themes + +Andromeda, Ayu, City Lights, Dracula, D.Va, GitHub, Graphite, Gruvbox, Monokai, Nord, One Dark, Pomotroid (default), Popping and Locking, Rose Pïne Dawn, Solarized Light, Spandex, Synthwave, Tokyo Night + +## Creating a custom theme + +A theme is a single `.json` file with a name and a set of hex color values. + +### 1. Create the themes directory + +The directory is not created automatically. Make it once: + +**Linux** +```sh +mkdir -p ~/.local/share/com.splode.pomotroid/themes +``` + +**macOS** +```sh +mkdir -p ~/Library/Application\ Support/com.splode.pomotroid/themes +``` + +**Windows** (PowerShell) +```powershell +New-Item -ItemType Directory -Force "$env:APPDATA\com.splode.pomotroid\themes" +``` + +### 2. Create a theme file + +Copy the template below into a `.json` file in the themes directory. The filename can be anything — the displayed name comes from the `name` field. + +```json +{ + "name": "My Theme", + "colors": { + "--color-focus-round": "#ff4e4d", + "--color-short-round": "#05ec8c", + "--color-long-round": "#0bbddb", + "--color-background": "#2f384b", + "--color-background-light": "#3d4457", + "--color-background-lightest": "#9ca5b5", + "--color-foreground": "#f6f2eb", + "--color-foreground-darker": "#c0c9da", + "--color-foreground-darkest": "#dbe1ef", + "--color-accent": "#05ec8c" + } +} +``` + +### 3. Select your theme + +Open **Settings → Appearance**. Your theme appears in the picker with a **Custom** badge. Select it for the Light or Dark slot (or both). + +## Color reference + +| Key | Used for | +|-----|----------| +| `--color-focus-round` | Work round indicator — dial arc, round dot | +| `--color-short-round` | Short break indicator | +| `--color-long-round` | Long break indicator | +| `--color-background` | Main window background | +| `--color-background-light` | Sidebar, cards, elevated surfaces | +| `--color-background-lightest` | Borders, dividers, subtler surfaces | +| `--color-foreground` | Primary text | +| `--color-foreground-darker` | Secondary text, labels | +| `--color-foreground-darkest` | Tertiary text, placeholders | +| `--color-accent` | Highlighted elements, active states | + +All values must be CSS hex colors (`#rrggbb` or `#rrggbbaa`). + +## Hot-reload + +Pomotroid watches the themes directory while running. Saving a file — including edits to an existing theme — updates the Appearance picker within half a second. There is no need to reopen settings or restart the app. + +## Overriding a built-in theme + +If a custom theme's `name` exactly matches a built-in theme name (case-insensitive), it replaces that theme in the picker. This lets you tweak an existing theme without adding a new entry to the list. + +## Using bundled themes as a starting point + +The bundled theme files are a useful reference. You can find them in the source repository under [`static/themes/`](./static/themes/). From d1873b60c432233f1142222a07cc729b02648010 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Fri, 27 Feb 2026 15:55:37 -0700 Subject: [PATCH 2/2] feat: Add version build suport. - Create version string during compile, CI/CD. --- .github/workflows/build.yml | 8 +- openspec/changes/build-version/.openspec.yaml | 2 + openspec/changes/build-version/design.md | 96 ++++++++++++ openspec/changes/build-version/proposal.md | 33 ++++ .../build-version/specs/build-version/spec.md | 64 ++++++++ openspec/changes/build-version/tasks.md | 46 ++++++ openspec/specs/localization/spec.md | 67 ++++++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 148 ++++++++++++++++++ src-tauri/src/commands.rs | 6 + src-tauri/src/lib.rs | 7 + .../settings/sections/AboutSection.svelte | 27 +++- src/lib/ipc/index.ts | 3 + 14 files changed, 503 insertions(+), 8 deletions(-) create mode 100644 openspec/changes/build-version/.openspec.yaml create mode 100644 openspec/changes/build-version/design.md create mode 100644 openspec/changes/build-version/proposal.md create mode 100644 openspec/changes/build-version/specs/build-version/spec.md create mode 100644 openspec/changes/build-version/tasks.md create mode 100644 openspec/specs/localization/spec.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7fe047..7361cba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build / Test on: push: - branches: [master, "rewrite/**", "claude/**"] + branches: [master, 'rewrite/**', 'claude/**', 'feat/**'] pull_request: branches: [master] @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 @@ -78,6 +80,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 @@ -120,6 +124,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/openspec/changes/build-version/.openspec.yaml b/openspec/changes/build-version/.openspec.yaml new file mode 100644 index 0000000..d1c6cc6 --- /dev/null +++ b/openspec/changes/build-version/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-27 diff --git a/openspec/changes/build-version/design.md b/openspec/changes/build-version/design.md new file mode 100644 index 0000000..c66d689 --- /dev/null +++ b/openspec/changes/build-version/design.md @@ -0,0 +1,96 @@ +## Context + +The About section of the settings window currently hardcodes `const VERSION = '1.0.0'` in `AboutSection.svelte`. This string is entirely disconnected from the actual build — there is no way to determine which commit a running binary was compiled from. Two binaries built from different commits show the same version string. + +`build.rs` already exists (`src-tauri/build.rs`) and contains only `tauri_build::build()`. It is the correct and idiomatic place to perform git introspection at compile time. + +The CI workflow uses `actions/checkout@v4` with default depth 1 (shallow clone), which prevents `git describe` from finding ancestor tags and computing commit distances. Full history is needed for commit count. + +## Goals / Non-Goals + +**Goals:** +- Every compiled binary carries a unique, traceable version string baked in at compile time. +- The string conforms to semver: `1.0.0-dev.{n}+{short-sha}` for dev builds, `1.0.0+{short-sha}` for release builds. +- Short SHA (7 chars) is displayed in Settings → About for readability. +- Full SHA is logged to the log file on startup for grep/traceability. +- `tauri.conf.json` remains the single source of truth for the base version number. +- Zero runtime overhead — the string is a compile-time constant. + +**Non-Goals:** +- Automatic version bumping or tag management. +- Exposing branch name in the version string (too noisy, changes often). +- A separate build number counter outside of git commit distance. +- Changing the `tauri.conf.json` version value at build time. + +## Decisions + +### D1: `build.rs` as the injection point + +**Decision**: Extend `src-tauri/build.rs` to run `git describe`, parse its output, and emit `cargo:rustc-env=APP_BUILD_VERSION=`. The string is accessed in Rust via the compile-time macro `env!("APP_BUILD_VERSION")`. + +**Alternatives considered**: +- *Vite `define` plugin*: Would work for the frontend, but the string would not be available in Rust (e.g., for startup logging). Split injection in two places introduces drift risk. +- *Runtime environment variable*: Requires the launching environment to set the variable; doesn't work for distributed binaries. +- *Patching `tauri.conf.json` before build*: Modifies a tracked file; requires cleanup; pollutes git diff. + +`build.rs` is the correct Rust idiom. `env!()` is zero-cost. One source, accessible everywhere. + +### D2: `git describe --tags --long --always --dirty` as the data source + +**Decision**: Use `git describe --tags --long --always --dirty`. + +- `--tags`: Match any tag (not just annotated). +- `--long`: Always output `{tag}-{count}-g{sha}` format, even when count is 0 (on the exact tag). Uniform parsing regardless of release/dev state. +- `--always`: Fall back to bare SHA if no tags exist at all. +- `--dirty`: Append `-dirty` if working tree has uncommitted changes. + +Output format: `v1.0.0-80-g20b2d87[-dirty]` + +Parsed into semver: +| count | dirty | result | +|-------|-------|--------| +| 0 | no | `1.0.0+20b2d87` | +| 0 | yes | `1.0.0+20b2d87.dirty` | +| N > 0 | no | `1.0.0-dev.N+20b2d87` | +| N > 0 | yes | `1.0.0-dev.N+20b2d87.dirty` | + +The `g` prefix from git describe is stripped; build metadata contains a clean 7-char hex SHA. + +**Fallback**: If `git describe` fails (no git binary, detached non-tagged repo), `build.rs` falls back to `{base_version}+unknown`. Base version is read from `tauri.conf.json` at build time. + +### D3: `cargo:rerun-if-changed` triggers + +**Decision**: Emit two rerun triggers: +``` +cargo:rerun-if-changed=.git/HEAD +cargo:rerun-if-changed=.git/refs/ +``` + +Without these, Cargo caches `build.rs` output and the version string becomes stale after commits. `.git/HEAD` changes on every commit and checkout. `.git/refs/` changes when tags are created or moved. + +### D4: Short SHA in UI, full SHA in logs + +**Decision**: The IPC command `app_version()` returns the version string with 7-char SHA (e.g., `1.0.0-dev.80+20b2d87`). A separate startup log line in `lib.rs` records the full 40-char SHA. + +The full SHA is captured via `git rev-parse HEAD` in `build.rs` and emitted as `APP_BUILD_SHA` alongside `APP_BUILD_VERSION`. + +### D5: New `app_version` Tauri command (no settings involvement) + +**Decision**: Expose the build version via a new read-only command `app_version() -> &'static str`. It is not a setting; it does not go through the settings system. The frontend calls it once on About section mount. + +### D6: CI checkout depth + +**Decision**: Add `fetch-depth: 0` to all three `actions/checkout@v4` steps in `build.yml`. This gives the CI full tag history, enabling commit count in CI-produced artifacts. Without it, `git describe` falls back to the SHA-only path and the commit count is absent. + +## Risks / Trade-offs + +- **`-dirty` in release binaries**: If someone builds a release from a dirty working tree, the binary is labeled `dirty`. This is accurate and intentional — it's a signal, not an error. +- **CI build time**: `fetch-depth: 0` fetches full history. On a project with few commits this is negligible. Worth monitoring if the repo grows very large. +- **No git binary at build time**: Unlikely in any normal dev or CI environment, but handled gracefully by the fallback to `+unknown`. +- **Shallow clone outside CI**: If someone clones with `--depth 1` locally and builds, they get `+unknown` or a bare SHA. Acceptable. + +## Migration Plan + +No runtime migration required. The change is entirely at compile time and in the UI display layer. No database changes, no settings changes, no breaking IPC changes. The new `app_version` command is additive. + +Rollout: merge to master, next build picks up the new version string automatically. diff --git a/openspec/changes/build-version/proposal.md b/openspec/changes/build-version/proposal.md new file mode 100644 index 0000000..a64b6ce --- /dev/null +++ b/openspec/changes/build-version/proposal.md @@ -0,0 +1,33 @@ +## Why + +The About screen currently shows a hardcoded `VERSION = '1.0.0'` string that provides no information about the exact build being run. When debugging issues, there is no way to tell which commit a binary was compiled from. A semver-conformant build identifier — baked in at compile time via `build.rs` — gives every binary a unique, traceable version string at zero runtime cost. + +## What Changes + +- `build.rs` is extended to call `git describe` at compile time, parse the output, and emit a `APP_BUILD_VERSION` environment variable baked into the binary. +- A new Tauri command `app_version()` exposes the build version string to the frontend. +- `AboutSection.svelte` replaces its hardcoded `VERSION` constant with an IPC call to `app_version()`. +- The full commit SHA is logged to the log file on startup, while the short SHA is displayed in the UI. +- The CI workflow gains `fetch-depth: 0` so commit counts are available in CI artifacts. +- `Cargo.toml` version is aligned to `1.0.0` to match `tauri.conf.json`. + +## Capabilities + +### New Capabilities + +- `build-version`: Compile-time build version string derived from `git describe`, formatted as semver with pre-release and build metadata. Exposed via IPC and displayed in Settings → About. + +### Modified Capabilities + +*(none — no existing spec-level requirements change)* + +## Impact + +- **`src-tauri/build.rs`**: New git describe logic + `cargo:rerun-if-changed` triggers. +- **`src-tauri/src/commands.rs`**: New `app_version` command. +- **`src-tauri/src/lib.rs`**: Log full SHA on startup. +- **`src/lib/ipc/index.ts`**: New `appVersion()` wrapper. +- **`src/lib/components/settings/sections/AboutSection.svelte`**: Replace hardcoded version with IPC-fetched value. +- **`.github/workflows/build.yml`**: Add `fetch-depth: 0` to all checkout steps. +- **`src-tauri/Cargo.toml`**: Align version to `1.0.0`. +- No new dependencies; no DB migrations; no settings changes. diff --git a/openspec/changes/build-version/specs/build-version/spec.md b/openspec/changes/build-version/specs/build-version/spec.md new file mode 100644 index 0000000..927d074 --- /dev/null +++ b/openspec/changes/build-version/specs/build-version/spec.md @@ -0,0 +1,64 @@ +## ADDED Requirements + +### Requirement: Build version string baked in at compile time +The system SHALL compute a semver-conformant build version string during `cargo build` by invoking `git describe --tags --long --always --dirty` in `build.rs` and emitting the result as the compile-time environment variable `APP_BUILD_VERSION`. The full commit SHA SHALL be emitted separately as `APP_BUILD_SHA` via `git rev-parse HEAD`. + +#### Scenario: Dev build (commits since last tag) +- **WHEN** the binary is compiled from a commit that is N > 0 commits after the most recent tag +- **THEN** `APP_BUILD_VERSION` SHALL be `{base}-dev.{N}+{short-sha}` (e.g. `1.0.0-dev.80+20b2d87`) + +#### Scenario: Release build (on exact tag) +- **WHEN** the binary is compiled from a commit that is exactly at a tag +- **THEN** `APP_BUILD_VERSION` SHALL be `{base}+{short-sha}` (e.g. `1.0.0+20b2d87`) + +#### Scenario: Dirty working tree +- **WHEN** the binary is compiled with uncommitted changes present +- **THEN** `APP_BUILD_VERSION` SHALL include a `.dirty` suffix in the build metadata (e.g. `1.0.0-dev.80+20b2d87.dirty`) + +#### Scenario: No git history or no tags +- **WHEN** `git describe` fails (no git binary, no tags, detached shallow clone) +- **THEN** `APP_BUILD_VERSION` SHALL fall back to `{base}+unknown` where `{base}` is read from `tauri.conf.json` + +#### Scenario: Incremental rebuild after a new commit +- **WHEN** a new commit is made and `cargo build` is run again +- **THEN** the build script SHALL re-execute and produce an updated `APP_BUILD_VERSION` reflecting the new commit + +--- + +### Requirement: Build version exposed via IPC command +The system SHALL provide a Tauri command `app_version` that returns `APP_BUILD_VERSION` as a `&'static str`. This command SHALL be callable by the frontend at any time and returns the compile-time-baked version string. + +#### Scenario: Command returns baked version +- **WHEN** the frontend invokes `app_version` +- **THEN** the response SHALL be the `APP_BUILD_VERSION` string compiled into the binary + +--- + +### Requirement: Build version displayed in Settings → About +The system SHALL display the build version string in Settings → About in place of the previously hardcoded version constant. The displayed string SHALL use the short-SHA form (7 characters). + +#### Scenario: Version displayed on About mount +- **WHEN** the user opens Settings → About +- **THEN** the version line SHALL show the full semver build string (e.g. `1.0.0-dev.80+20b2d87`) + +#### Scenario: Version string is never empty +- **WHEN** `app_version` returns successfully +- **THEN** the About section SHALL display the returned string; if the call fails, it SHALL fall back to displaying the base version from `tauri.conf.json` + +--- + +### Requirement: Full commit SHA logged on startup +The system SHALL log the full 40-character commit SHA (from `APP_BUILD_SHA`) at INFO level during application startup, alongside the build version string. + +#### Scenario: Startup log includes full SHA +- **WHEN** the application starts +- **THEN** the log file SHALL contain an INFO entry with both the build version string and the full commit SHA (e.g. `[app] version=1.0.0-dev.80+20b2d87 sha=20b2d87173870d939002efe84fddff2e944eabd6`) + +--- + +### Requirement: CI workflow fetches full git history +The CI build workflow SHALL use `fetch-depth: 0` on all checkout steps so that `git describe` has access to full tag history and can compute commit distances in CI-produced artifacts. + +#### Scenario: CI artifact carries commit count +- **WHEN** a binary is built in CI from a commit that is N commits after the last tag +- **THEN** the binary's `APP_BUILD_VERSION` SHALL include the commit count N (e.g. `1.0.0-dev.80+abc1234`) diff --git a/openspec/changes/build-version/tasks.md b/openspec/changes/build-version/tasks.md new file mode 100644 index 0000000..9e761a3 --- /dev/null +++ b/openspec/changes/build-version/tasks.md @@ -0,0 +1,46 @@ +## 1. Build Script + +- [x] 1.1 In `src-tauri/build.rs`, add a function that runs `git describe --tags --long --always --dirty` and captures stdout +- [x] 1.2 Parse the describe output into its components: base version tag, commit count, short SHA, dirty flag +- [x] 1.3 Read the base version from `tauri.conf.json` as the fallback source of truth +- [x] 1.4 Implement the semver formatting logic: `{base}-dev.{n}+{sha}` for dev, `{base}+{sha}` for release, `.dirty` suffix when applicable +- [x] 1.5 Handle the fallback case (`git describe` fails or no tags): emit `{base}+unknown` +- [x] 1.6 Run `git rev-parse HEAD` and capture the full 40-char SHA +- [x] 1.7 Emit `cargo:rustc-env=APP_BUILD_VERSION=` with the computed semver string +- [x] 1.8 Emit `cargo:rustc-env=APP_BUILD_SHA=` with the full commit SHA +- [x] 1.9 Emit `cargo:rerun-if-changed=.git/HEAD` to invalidate cache on new commits +- [x] 1.10 Emit `cargo:rerun-if-changed=.git/refs/` to invalidate cache on tag changes +- [x] 1.11 Keep the existing `tauri_build::build()` call intact + +## 2. Rust Backend + +- [x] 2.1 In `src-tauri/src/commands.rs`, add `app_version` command that returns `env!("APP_BUILD_VERSION")` as `&'static str` +- [x] 2.2 Register `app_version` in the `tauri::Builder::invoke_handler` list in `src-tauri/src/lib.rs` +- [x] 2.3 In `src-tauri/src/lib.rs` startup, add an INFO log entry: `[app] version={APP_BUILD_VERSION} sha={APP_BUILD_SHA}` +- [x] 2.4 Align `src-tauri/Cargo.toml` version to `1.0.0` to match `tauri.conf.json` + +## 3. Frontend IPC + +- [x] 3.1 In `src/lib/ipc/index.ts`, add `appVersion(): Promise` wrapper that invokes `app_version` + +## 4. About Section + +- [x] 4.1 In `AboutSection.svelte`, remove the hardcoded `const VERSION = '1.0.0'` constant +- [x] 4.2 Add `import { appVersion } from '$lib/ipc'` +- [x] 4.3 Declare `let version = $state('...')` with a loading placeholder +- [x] 4.4 In `onMount`, call `appVersion()` and set `version` from the result; on error, fall back to the base version from `tauri.conf.json` +- [x] 4.5 Update the version display to use the reactive `version` state variable +- [x] 4.6 Update `RELEASE_URL` to derive the tag from the base version only (strip pre-release/build metadata before constructing the GitHub releases URL) + +## 5. CI Workflow + +- [x] 5.1 In `.github/workflows/build.yml`, add `fetch-depth: 0` to the Linux `actions/checkout@v4` step +- [x] 5.2 Add `fetch-depth: 0` to the macOS `actions/checkout@v4` step +- [x] 5.3 Add `fetch-depth: 0` to the Windows `actions/checkout@v4` step + +## 6. Verification + +- [x] 6.1 Run `cargo check` in `src-tauri/` — no errors +- [x] 6.2 Run `npm run check` — no type errors +- [ ] 6.3 Run `npm run tauri dev`, open Settings → About, confirm version string shows git-derived value (not `1.0.0`) +- [ ] 6.4 Confirm the log file on startup contains a line with the full SHA diff --git a/openspec/specs/localization/spec.md b/openspec/specs/localization/spec.md new file mode 100644 index 0000000..df9028f --- /dev/null +++ b/openspec/specs/localization/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Paraglide-based message catalog +The system SHALL use Paraglide JS v2 (`@inlang/paraglide-js`) to manage all user-visible strings. All strings SHALL be defined in message files (`messages/.json`) and accessed through generated type-safe message functions (`m.()`). The base locale SHALL be `en`. + +#### Scenario: Type-safe message access +- **WHEN** a developer references a message key that does not exist +- **THEN** TypeScript SHALL report a compile error + +#### Scenario: Unused message tree-shaking +- **WHEN** the app is built for production +- **THEN** message functions for unused keys SHALL be eliminated from the bundle + +### Requirement: Supported locales at launch +The system SHALL ship five locales: English (`en`, base), Spanish (`es`), French (`fr`), German (`de`), and Japanese (`ja`). Non-English locales MAY be machine-translated. All locale message files SHALL contain translations for every key defined in `messages/en.json`. + +#### Scenario: All keys present in non-English locales +- **WHEN** a non-English message file is loaded +- **THEN** every key defined in `messages/en.json` SHALL have a corresponding entry + +#### Scenario: Fallback to English for missing keys +- **WHEN** a message key is missing in the active locale's file +- **THEN** the English string SHALL be displayed as a fallback + +### Requirement: Automatic locale detection +The system SHALL default to `language = 'auto'`. When `'auto'` is active, the locale SHALL be resolved from `navigator.language` by matching the closest supported locale (prefix match). If no match is found, the locale SHALL fall back to `en`. + +#### Scenario: Exact locale match +- **WHEN** `navigator.language` is `'fr'` +- **THEN** the active locale SHALL be `fr` + +#### Scenario: Region-qualified locale match +- **WHEN** `navigator.language` is `'de-AT'` +- **THEN** the active locale SHALL be `de` + +#### Scenario: Unsupported locale fallback +- **WHEN** `navigator.language` is `'zh-CN'` +- **THEN** the active locale SHALL fall back to `en` + +### Requirement: User language override +The system SHALL allow the user to override the detected locale via a language dropdown in the System settings section. The selected locale SHALL be persisted as the `language` setting and applied immediately without requiring an app restart. + +#### Scenario: User selects a specific language +- **WHEN** the user selects `'fr'` from the language dropdown +- **THEN** all UI strings in both windows SHALL immediately display in French + +#### Scenario: User resets to automatic detection +- **WHEN** the user selects `'Auto'` from the language dropdown +- **THEN** `language` is saved as `'auto'` and the locale is re-resolved from `navigator.language` + +### Requirement: Locale applied in both windows +The system SHALL apply the active locale in both the main timer window and the settings window. When the `language` setting changes, both windows SHALL re-call `setLocale()` in response to the `settings:changed` event. + +#### Scenario: Language change propagates to both windows +- **WHEN** the user changes the language setting while the settings window is open +- **THEN** both the timer window and the settings window SHALL update their displayed strings + +### Requirement: Translated desktop notifications +The system SHALL send desktop notifications with titles and bodies constructed from translated Paraglide message strings. Notification string construction SHALL happen on the frontend; the Rust backend SHALL receive a pre-translated `title` and `body`. + +#### Scenario: Work round complete notification in active locale +- **WHEN** a work round completes and the active locale is `fr` +- **THEN** the notification title and body SHALL be in French + +#### Scenario: Notification Rust command accepts arbitrary title and body +- **WHEN** `notification_show(title, body)` is called from the frontend +- **THEN** Rust SHALL display the notification with the provided title and body without any string construction diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 139326c..2aeae0b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3304,7 +3304,7 @@ dependencies = [ [[package]] name = "pomotroid" -version = "0.1.0" +version = "1.0.0" dependencies = [ "axum", "futures-util", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4d81db7..a0e05eb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pomotroid" -version = "0.1.0" +version = "1.0.0" description = "A simple and visually-pleasing Pomodoro timer" authors = ["Christopher Murphy "] edition = "2021" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e..68251bf 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,151 @@ +use std::process::Command; + fn main() { + // Rerun when git HEAD changes (new commit, checkout) or when tags move. + println!("cargo:rerun-if-changed=.git/HEAD"); + println!("cargo:rerun-if-changed=.git/refs/"); + // Rerun if the canonical version source changes. + println!("cargo:rerun-if-changed=tauri.conf.json"); + + let base_version = read_base_version(); + + // Full 40-char SHA for the log line. + let full_sha = run_git(&["rev-parse", "HEAD"]) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Build version string from git describe. + let build_version = match run_git(&["describe", "--tags", "--long", "--always", "--dirty"]) { + Some(raw) => { + let raw = raw.trim(); + match parse_describe(raw) { + Some(info) => format_version(&info), + // --always fallback: no tags, describe returned just a raw SHA. + None => format_fallback_from_sha(&base_version, raw), + } + } + None => format!("{base_version}+unknown"), + }; + + println!("cargo:rustc-env=APP_BUILD_VERSION={build_version}"); + println!("cargo:rustc-env=APP_BUILD_SHA={full_sha}"); + tauri_build::build() } + +// --------------------------------------------------------------------------- +// Git helpers +// --------------------------------------------------------------------------- + +fn run_git(args: &[&str]) -> Option { + Command::new("git") + .args(args) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) +} + +// --------------------------------------------------------------------------- +// Version parsing +// --------------------------------------------------------------------------- + +struct DescribeInfo { + tag: String, + count: u32, + sha: String, + dirty: bool, +} + +/// Parse `git describe --tags --long --always --dirty` output. +/// +/// Expected form: `v1.0.0-80-g20b2d87[-dirty]` +/// Uses rsplitn so tag names containing hyphens are handled correctly. +fn parse_describe(s: &str) -> Option { + let (s, dirty) = s + .strip_suffix("-dirty") + .map(|t| (t, true)) + .unwrap_or((s, false)); + + // rsplitn(3) from the right: ["g20b2d87", "80", "v1.0.0"] + let parts: Vec<&str> = s.rsplitn(3, '-').collect(); + if parts.len() != 3 { + return None; + } + + let sha_part = parts[0]; // "g20b2d87" + let count_part = parts[1]; // "80" + let tag_part = parts[2]; // "v1.0.0" + + let count = count_part.parse::().ok()?; + let sha = sha_part.strip_prefix('g')?; + let tag = tag_part.trim_start_matches('v'); + + Some(DescribeInfo { + tag: tag.to_string(), + count, + sha: sha.to_string(), + dirty, + }) +} + +/// Format a parsed describe into a semver string. +/// +/// | count | dirty | result | +/// |-------|-------|---------------------------------| +/// | 0 | no | `1.0.0+20b2d87` | +/// | 0 | yes | `1.0.0+20b2d87.dirty` | +/// | N > 0 | no | `1.0.0-dev.N+20b2d87` | +/// | N > 0 | yes | `1.0.0-dev.N+20b2d87.dirty` | +fn format_version(info: &DescribeInfo) -> String { + let base = if info.count == 0 { + format!("{}+{}", info.tag, info.sha) + } else { + format!("{}-dev.{}+{}", info.tag, info.count, info.sha) + }; + if info.dirty { + format!("{base}.dirty") + } else { + base + } +} + +/// Fallback when no tags exist: describe returned a bare SHA (from --always). +/// Input looks like `20b2d87` or `20b2d87-dirty`. +fn format_fallback_from_sha(base_version: &str, raw: &str) -> String { + let (sha, dirty) = raw + .strip_suffix("-dirty") + .map(|s| (s, true)) + .unwrap_or((raw, false)); + // Strip 'g' prefix if somehow present. + let sha = sha.strip_prefix('g').unwrap_or(sha); + if dirty { + format!("{base_version}+{sha}.dirty") + } else { + format!("{base_version}+{sha}") + } +} + +// --------------------------------------------------------------------------- +// Base version source of truth +// --------------------------------------------------------------------------- + +/// Read "version" from tauri.conf.json (same directory as build.rs = src-tauri/). +/// Falls back to "1.0.0" if the file is unreadable or the field is missing. +fn read_base_version() -> String { + std::fs::read_to_string("tauri.conf.json") + .ok() + .and_then(|content| extract_json_str_field(&content, "version")) + .unwrap_or_else(|| "1.0.0".to_string()) +} + +/// Minimal JSON string-field extractor — avoids a serde_json build-dependency. +fn extract_json_str_field(json: &str, field: &str) -> Option { + let key = format!("\"{}\"", field); + let pos = json.find(&key)?; + let rest = json[pos + key.len()..].trim_start(); + let rest = rest.strip_prefix(':')?.trim_start(); + let rest = rest.strip_prefix('"')?; + let end = rest.find('"')?; + Some(rest[..end].to_string()) +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7c5354f..23ea514 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -441,6 +441,12 @@ pub fn open_log_dir(app: AppHandle) { } } +/// Return the compile-time build version string. +#[tauri::command] +pub fn app_version() -> &'static str { + env!("APP_BUILD_VERSION") +} + /// Return the application log directory path as a string. #[tauri::command] pub fn get_log_dir(app: AppHandle) -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1a0dad8..9098b47 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ use tauri::Manager; use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind}; use commands::{ + app_version, audio_clear_custom, audio_get_custom_info, audio_set_custom, get_log_dir, open_log_dir, notification_show, @@ -59,6 +60,11 @@ pub fn run() { // --- Database --- let db = match db::open(&app_data_dir) { Ok(d) => { + log::info!( + "[app] version={} sha={}", + env!("APP_BUILD_VERSION"), + env!("APP_BUILD_SHA") + ); log::info!( "Pomotroid v{} — data dir: {}", env!("CARGO_PKG_VERSION"), @@ -215,6 +221,7 @@ pub fn run() { // Diagnostics open_log_dir, get_log_dir, + app_version, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/lib/components/settings/sections/AboutSection.svelte b/src/lib/components/settings/sections/AboutSection.svelte index 8e8d04c..416fe8e 100644 --- a/src/lib/components/settings/sections/AboutSection.svelte +++ b/src/lib/components/settings/sections/AboutSection.svelte @@ -1,12 +1,29 @@
@@ -33,12 +50,12 @@

Pomotroid

-

Version {VERSION}

+

Version {version}