Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .agents/skills/improved_sdd_tdd_cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ understanding more than once:
| **Peer-tier delegation** | An agent as strong as the Lead redoes what the Lead already knows. | Delegate *down* or not at all. §4 |
| **Reading to answer a grep** | Whole files loaded to establish one fact. | Ask the shell first. §10 |
| **Regex work sent to a model** | A model reasons through what `sed` does for free. | Script it. §7, rule S |
| **Full-suite runs while iterating** | `go test -timeout 20m -race ./...` × every loop. | Targeted packages until the final gate. §10 |
| **Full-suite runs while iterating** | `go test -timeout 30m -race ./...` × every loop. | Targeted packages until the final gate. §10 |
| **Verifying by re-reading** | Re-derive from the diff what one command would have told you. | Evidence is command output. §10 |

The one delegation that *saves* Lead context rather than spending it is
Expand Down Expand Up @@ -379,7 +379,7 @@ say so plainly.
| "It works on Windows" | A Windows run, or an explicit statement that it is unverified |

**While iterating:** targeted packages only — `go test ./internal/update/`,
`go test ./internal/ui/ -run TestUpdateFailure`. The full `-race` suite is 20
`go test ./internal/ui/ -run TestUpdateFailure`. The full `-race` suite is 30
minutes; running it every loop is pure cost with no new information.

**Final gate, once, before handoff** (matches CI — `AGENTS.md §Build and Verification`):
Expand All @@ -388,7 +388,7 @@ minutes; running it every loop is pure cost with no new information.
make fmt-check
go vet ./...
go build ./...
go test -timeout 20m -race ./...
go test -timeout 30m -race ./...
```

Cross-platform work adds `GOOS=windows GOARCH=amd64 go vet ./internal/...`.
Expand Down
6 changes: 3 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ participating, you're expected to uphold it.

## Getting set up

- Go 1.26.6 or newer (see the `go` directive in [go.mod](../go.mod))
- Go 1.27.1 or newer (see the `go` directive in [go.mod](../go.mod))
- A C toolchain for cgo (Fyne's OpenGL bindings require it) — Xcode Command
Line Tools on macOS, `gcc` + `libgl1-mesa-dev`/`xorg-dev` on Linux
- See the [README](../README.md#requirements) for the full list, including
Expand Down Expand Up @@ -43,7 +43,7 @@ make run
```sh
make fmt-check # goimports -local; should print nothing / exit 0
go vet ./...
go test -timeout 20m -race ./...
go test -timeout 30m -race ./...
```

Or via the [Makefile](../Makefile): `make fmt`, `make vet`, `make test`.
Expand Down Expand Up @@ -74,7 +74,7 @@ make run
- Open the PR against `main` and fill in the pull request template.
- Keep the change focused — unrelated cleanup makes review harder and is
easier to land as its own PR.
- CI (`goimports -local`, `go vet`, `go build`, `go test -timeout 20m -race`) must pass.
- CI (`goimports -local`, `go vet`, `go build`, `go test -timeout 30m -race`) must pass.
- A maintainer will review and may ask for changes before merging.

## Reporting bugs and requesting features
Expand Down
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

## Checklist

- [ ] `make fmt-check` is clean, `go vet ./...` and `go test -timeout 20m -race ./...` pass
- [ ] `make fmt-check` is clean, `go vet ./...` and `go test -timeout 30m -race ./...` pass
- [ ] User-visible strings go through `lang.L`, with the key added to every
bundle in `translations/`
- [ ] `internal/ui/help/manual.md` and `manual_de.md` updated, if this
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ jobs:
# BCP-47 tag, so Fyne logs a three-line parse fault before any test
# runs. Naming a real one keeps that noise out of the test output.
#
# -timeout 20m: go test defaults to 10m per package. internal/ui on
# -timeout 30m: go test defaults to 10m per package. internal/ui on
# ubuntu-latest already took ~9m39s before folder-sibling tests; the
# next commit then panicked at 10m0s while a later test was starting.
env:
LANG: en_US.UTF-8
run: go test -timeout 20m -race ./...
run: go test -timeout 30m -race ./...

windows-test:
runs-on: windows-latest
Expand Down
119 changes: 114 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,29 +91,138 @@ jobs:
tar -czf "picfetch-linux-$arch.tar.gz" "picfetch-linux-$arch"
done

- name: Upload Windows/Linux artifacts
- name: Upload unsigned Windows artifacts for signing
uses: actions/upload-artifact@v7
with:
name: picfetch-cross
name: picfetch-windows-unsigned
path: |
bin/picfetch-windows-amd64.zip
bin/picfetch-windows-arm64.zip
if-no-files-found: error

- name: Upload Linux artifacts
uses: actions/upload-artifact@v7
with:
name: picfetch-linux
path: |
bin/picfetch-linux-amd64.tar.gz
bin/picfetch-linux-arm64.tar.gz
if-no-files-found: error

sign-windows:
# Keep the code-signing credentials behind the protected GitHub Environment
# configured in docs/release-signing.md. This job signs only tag-release
# artifacts after the test gate and must finish before publication.
needs: build-cross
runs-on: windows-latest
environment: release-signing
permissions:
contents: read
steps:
- name: Download unsigned Windows artifacts
uses: actions/download-artifact@v8
with:
name: picfetch-windows-unsigned
path: dist/windows-unsigned

- name: Download and verify Certum SimplySign Desktop
shell: pwsh
run: |
$installerUrl = 'https://files.certum.eu/software/SimplySignDesktop/Windows/9.4.4.92/SimplySignDesktop-9.4.4.92-64-bit-en.msi'
Invoke-WebRequest -Uri $installerUrl -OutFile SimplySignDesktop.msi -ErrorAction Stop

$signature = Get-AuthenticodeSignature -FilePath SimplySignDesktop.msi
if ($signature.Status -ne 'Valid') {
throw "SimplySign installer has an invalid Authenticode signature: $($signature.Status)."
}
Write-Host "Verified SimplySign installer publisher: $($signature.SignerCertificate.Subject)"

- name: Authenticate Certum SimplySign
# This commit is the reviewed v1 release of the third-party action.
# Do not replace it with a floating tag: it receives the TOTP secret.
uses: dismine/windows-app-signing-setup-action@89ae3b032d4bc7a5b98d1a42a34e61ecb6faad64 # v1
with:
certum-username: ${{ secrets.CERTUM_USERNAME }}
certum-otp-uri: ${{ secrets.CERTUM_OTP_URI }}
certum-key-id: ${{ secrets.CERTUM_CERT_THUMBPRINT }}
# Install the verified local MSI; fail if it is missing instead of downloading a fallback.
simplysign-url: https://simplysign-installer.invalid/SimplySignDesktop.msi
Comment thread
frathe marked this conversation as resolved.

- name: Sign, timestamp, and verify Windows executables
shell: pwsh
env:
CERTUM_CERT_THUMBPRINT: ${{ secrets.CERTUM_CERT_THUMBPRINT }}
run: |
$signtool = Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\bin' -Filter signtool.exe -Recurse |
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
Select-Object -First 1
if ($null -eq $signtool) {
throw 'SignTool.exe was not found in the Windows SDK.'
}

$archives = @(Get-ChildItem -Path dist/windows-unsigned -Filter 'picfetch-windows-*.zip' -File)
if ($archives.Count -ne 2) {
throw "Expected exactly two Windows archives, found $($archives.Count)."
}

New-Item -ItemType Directory -Force -Path dist/windows-signed | Out-Null
foreach ($archive in $archives) {
$unpacked = Join-Path dist/unpacked $archive.BaseName
Expand-Archive -Path $archive.FullName -DestinationPath $unpacked -Force
$executable = Join-Path $unpacked 'picfetch.exe'
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
throw "Expected executable is missing from $($archive.Name)."
}

& $signtool.FullName sign /fd sha256 /tr http://time.certum.pl /td sha256 /sha1 $env:CERTUM_CERT_THUMBPRINT /v $executable
if ($LASTEXITCODE -ne 0) {
throw "Signing failed for $($archive.Name)."
}
& $signtool.FullName verify /pa /all /v /tw $executable
if ($LASTEXITCODE -ne 0) {
throw "Signature verification failed for $($archive.Name)."
}

Compress-Archive -Path $executable -DestinationPath (Join-Path dist/windows-signed $archive.Name) -Force
}

- name: Upload verified signed Windows artifacts
uses: actions/upload-artifact@v7
with:
name: picfetch-windows-signed
path: dist/windows-signed/picfetch-windows-*.zip
if-no-files-found: error

release:
needs: [build-macos, build-cross]
needs: [build-macos, build-cross, sign-windows]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v7

- name: Download artifacts
- name: Download macOS artifacts
uses: actions/download-artifact@v8
with:
name: picfetch-macos-arm64
path: dist

- name: Download Intel macOS artifacts
uses: actions/download-artifact@v8
with:
name: picfetch-macos-x86_64
path: dist

- name: Download Linux artifacts
uses: actions/download-artifact@v8
with:
name: picfetch-linux
path: dist

- name: Download signed Windows artifacts
uses: actions/download-artifact@v8
with:
name: picfetch-windows-signed
path: dist
merge-multiple: true

- name: Create GitHub release
uses: softprops/action-gh-release@v3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 01 - Route Swipe input by revealed pane

Status: resolved

## Contract

Through `compare.Feature.Overlay()` in a real Fyne test window, prove that
Swipe + Unlinked comparison assigns pointer input to the photo occupying the
revealed region under the pointer. Hit regions must follow the current divider
without changing either photo's full-viewport render geometry.

Add a private `layoutPaneInput(index, input)` helper driven by
`paneVisibleArea`. Apply it during pane layout and every reveal/divider update,
and remove the full-viewport input reset from transform application. The
divider remains the exclusive drag target in its hit area, and a fully hidden
pane has no interactive area.

Files: `internal/ui/compare/compare_test.go`,
`internal/ui/compare/transform.go`, and `internal/ui/compare/swipe.go`.

## Red / green

1. Add `TestCompareSwipeUnlinkedCanvasRoutesPointerByReveal` using actual
canvas hover, drag, and wheel dispatch at the default divider and after
moving it to 75%.
2. Observe the current implementation report `Unlinked: Right` while the
pointer is over the visible left photo.
3. Implement reveal-aligned pane input bounds.
4. Verify Left/Right status, pane-local gestures, and subsequent transform keys
affect only the revealed target. Retain the last target after leaving a
photo region.

## Acceptance

`go test ./internal/ui/compare -run '^TestCompareSwipeUnlinkedCanvasRoutesPointerByReveal$' -count=1`

## Constraints

- Do not change renderer viewports, reveal clips, image transforms, tile
planning, shaders, caches, or divider behavior.
- Do not add an assembled-viewer duplicate of this regression.
- Do not add exported APIs or user-visible strings.

## Comments

- Red: the permanent canvas test reported `Unlinked: Right` while the pointer
was at x=200 in the visible left reveal.
- Green: reveal-aligned pane input bounds passed the focused acceptance command,
including divider movement, both extremes, gestures, and transform keys.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 02 - Preserve the right Swipe wheel anchor

Status: resolved
Blocked by: 01

## Contract

After Ticket 01 makes the right pane input start at the divider, preserve the
full-viewport image point beneath an unmodified wheel gesture. Copy each
non-nil `fyne.ScrollEvent`, add the input widget's reveal offset to the copied
event position, and forward that viewport-relative event. Never mutate the
event supplied by the caller.

Files: `internal/ui/compare/compare_test.go` and
`internal/ui/compare/input.go`.

## Red / green

1. Add `TestCompareSwipeUnlinkedRightWheelPreservesViewportAnchor` through the
overlay's pane input seam after Ticket 01 is green.
2. Observe the right photo zoom around the reveal-local coordinate instead of
the full-viewport cursor position.
3. Add the scroll-coordinate translation and observe the point beneath the
cursor remain fixed.
4. Verify the original event is unchanged and nil events remain inert.

## Acceptance

`go test ./internal/ui/compare -run '^TestCompareSwipeUnlinkedRightWheelPreservesViewportAnchor$' -count=1`

## Constraints

- Preserve left-pane, side-by-side, linked-wheel, and Shift+wheel behavior.
- Do not expose pane internals or add a second scroll path.
- Do not mutate caller-owned input events.

## Comments

- Red: with reveal-local x=100 forwarded unchanged, the normalized point under
full-viewport x=500 moved from `0.625` to `0.5774` during wheel zoom.
- Green: translating a copied event by the input origin preserved the anchor;
the original event remained unchanged and nil stayed inert.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 03 - Document, review, and verify the Swipe routing fix

Status: resolved
Blocked by: 01, 02

## Contract

Record the approved terminology and implementation invariant, review the two
vertical TDD slices, negatively verify their guards, and run the final gate
once.

Add **Linked comparison** and **Unlinked comparison** to `CONTEXT.md` and mark
locked/unlocked comparison as avoided terminology. Update `ARCHITECTURE.md` to
state that Swipe input bounds mirror the reveal while wheel coordinates are
translated back into the full viewport. Add the bugfix to `todos.md` and
normalize its existing locking/unlocking wording to linking/unlinking.

Complete the local spec and ticket comments with observed evidence, record the
Standard-route plan and cost ledger, and move the completed plan to
`finished_refactorings/` after the final gate.

## Verification

1. Run `go test ./internal/ui/compare -count=1`.
2. Run
`go test ./internal/ui -run 'Compare(LinkToggle|SwipePointer)' -count=1`.
3. Temporarily restore full-width pane inputs and confirm Ticket 01 fails for
the original Right-over-left symptom; restore the fix.
4. Temporarily remove scroll-coordinate translation and confirm Ticket 02
fails for lost cursor anchoring; restore the fix.
5. Rerun both focused ticket commands on the restored tree.
6. Run `make verify` once and record its actual result.

## Acceptance

- Every spec acceptance command passes on the final tree.
- `rg -n 'Linked comparison|Unlinked comparison' CONTEXT.md` finds both
canonical terms.
- `rg -n 'reveal|revealed' ARCHITECTURE.md todos.md` finds the architecture and
release-note records.
- `make verify` passes.
- No diagnostic files or debug instrumentation remain.

## Constraints

- Leave the already-correct manuals and translations unchanged.
- Do not create an ADR or claim a manual native UI smoke test.
- Do not commit; provide the suggested commit message at handoff.

## Comments

- `go test ./internal/ui/compare -count=1` passed, as did the assembled
`Compare(LinkToggle|SwipePointer)` selection and both focused guards.
- Deliberately restoring full-width inputs reproduced `Unlinked: Right` over
the left reveal. Deliberately removing coordinate translation reproduced the
wheel-anchor drift from `0.625` to `0.5774`. Both fixes were restored and
both guards passed again.
- `CONTEXT.md`, `ARCHITECTURE.md`, and `todos.md` now record the approved terms,
invariant, and bugfix. Manuals, translations, and ADRs were left unchanged.
- `make verify` passed: formatting, embedded TUF-root check, vet, build, and the
complete Linux/amd64 race suite were green (`internal/ui` 676.609s;
`internal/ui/compare` 28.486s).
Loading