Skip to content

Commit 84b90d6

Browse files
donislawdevclaude
andcommitted
gui: a Donate button, and rule 8 says in words why that is allowed
The tool takes donations towards its development now. A button at the top right of the window opens the support page, and .github/FUNDING.yml puts the same address behind GitHub's own sponsor button. The button is window chrome rather than part of a screen. It sits above the tabs so it is reachable from all four, and at the right edge because that is where this window already puts what you press rather than what you read. Host gains OpenLink beside ChooseDirectory, for the same reason that one is there: it is a thing only a real window can do, so a stand in records the address instead and a guard can press the button on a machine with no browser. Untouchable rule 8 says zero outgoing connections, so this needed the rule to say what it means before it needed code. The owner's clarification, the second in one day: what is forbidden is a connection the program makes ITSELF - telemetry, update checks, cloud clients, downloading. Handing an address to the desktop, on a press somebody made and with nothing sent, is not one. It is written into the rule rather than kept as an exception in another file, because a rule that does not describe what it permits is a rule the next session reads as broken. The boundary stays machine watched rather than left to care. internal/gui still may not import a network package, so fetching that page instead of opening it would need net/http and would redden the guard that already watches for it. net/url is a parser and is not on that list. Two guards. One presses the button and checks where it goes: a button that opens the wrong address looks identical from inside this program and is only ever found by somebody landing on a 404 while trying to pay. The other holds the address against the sponsor file, because two places now name where the money goes and nothing else links them. The focus chain guard learned the difference between window chrome and a leak, and learned it by name rather than by tolerance. Donate is allowed in every screen's chain and has to be reachable from each, while focus wandering into another tab is still the defect it always was. Also settles a contradiction found while writing this up. GIT.md said the changelog has exactly six categories and forbade inventing more, while CHANGELOG.md had been using a seventh for a long time. Breaking stays and the rule now names it: with a major bump being a statement about hashes in other people's CI, "PDF bytes have changed" is what a reader looks for first, and burying it in Changed hides the one entry that matters most. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b9b13dd commit 84b90d6

58 files changed

Lines changed: 9052 additions & 8552 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/FUNDING.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# The sponsor button GitHub shows on the repository.
2+
#
3+
# One custom address rather than a platform, because the page behind it is ours
4+
# and can say what the money is for. The key and the quoting are from GitHub's
5+
# own documentation, checked on 2026-08-18: a custom URL holding a colon has to
6+
# be quoted, which every https address does.
7+
custom: ["https://donislawdev.com/support/"]

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ because it turns other people's test suites red.
1515
## [Unreleased]
1616

1717
### Added
18+
- **A Donate button, top right of the window.** It opens the support page in your
19+
browser. The tool is free and stays free - this pays for the time that goes into it.
20+
21+
The program fetches nothing and sends nothing. It hands the address to your desktop,
22+
on a press you made, and that is all it does.
23+
1824
- **A third screen in the window: several batches in one run.** Until now the window
1925
produced one batch of files at a time, and everything else a recipe can ask for was
2026
reachable only from the command line. The new screen holds a list of batches and runs

‎internal/guard/reachability_test.go‎

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package guard
22

33
import (
44
"fmt"
5-
5+
"os"
6+
"path/filepath"
7+
"strings"
68
"testing"
79

810
"fyne.io/fyne/v2"
@@ -227,12 +229,42 @@ func TestTabbingReachesTheControlsAndSaysInWhatOrder(t *testing.T) {
227229
// everything can be reached, and that the chain stays on the
228230
// screen a person is looking at. The order is logged, so a change
229231
// to it is visible in a run rather than silent.
232+
// The chain also holds what belongs to the WINDOW rather than to a
233+
// screen, and since 2026-08-18 there is such a control: Donate sits
234+
// above the tabs and is reachable from every one of them. That is
235+
// right rather than a leak - UX9 asks that everything reachable with
236+
// a mouse is reachable from the keyboard, and this button is on the
237+
// screen whichever tab is open.
238+
//
239+
// Named rather than tolerated. A blanket "anything outside the tab is
240+
// fine" would have turned this assertion off, and the thing it
241+
// catches - focus wandering into the OTHER tabs, which are in the
242+
// canvas and laid out - is exactly what it was written for.
243+
chrome := map[string]bool{text.ButtonDonate: true}
244+
for _, f := range order {
245+
if onScreen[f] {
246+
continue
247+
}
248+
if button, ok := f.(*widget.Button); ok && chrome[button.Text] {
249+
continue
250+
}
251+
t.Errorf("Tab reaches %s, which is not on the %q screen. Focus is leaving "+
252+
"the screen a person is looking at", describeFocusable(f), tab)
253+
}
254+
255+
// And the window chrome has to be reachable at all, from every
256+
// screen, or the button nobody can Tab to is the one asking for
257+
// money.
258+
reachedChrome := false
230259
for _, f := range order {
231-
if !onScreen[f] {
232-
t.Errorf("Tab reaches %s, which is not on the %q screen. Focus is leaving "+
233-
"the screen a person is looking at", describeFocusable(f), tab)
260+
if button, ok := f.(*widget.Button); ok && chrome[button.Text] {
261+
reachedChrome = true
234262
}
235263
}
264+
if !reachedChrome {
265+
t.Errorf("the %q button cannot be reached with Tab from the %q screen (UX9)",
266+
text.ButtonDonate, tab)
267+
}
236268
})
237269
}
238270
}
@@ -296,3 +328,65 @@ func TestEveryTabInTheWindowIsOnTheListGuardsWalk(t *testing.T) {
296328
len(inWindow), len(allTabs()), inWindow, allTabs())
297329
}
298330
}
331+
332+
// The Donate button opens the support page, and it opens nothing else.
333+
//
334+
// Two things are asserted and the second is the one that would go wrong quietly.
335+
// A button that opens no address at all is obvious the first time anybody presses
336+
// it. A button that opens the WRONG address - a typo in the host, a stale page, a
337+
// path that has moved - looks identical from the inside of this program and is
338+
// only ever discovered by somebody landing on a 404 while trying to give money.
339+
//
340+
// It also pins the shape of untouchable rule 8. The program hands an address to
341+
// the desktop and sends nothing, which is what the carve out written into that
342+
// rule on 2026-08-18 permits. A press that instead fetched something would need a
343+
// network package inside internal/gui, and TestOurOwnWindowCodeDoesNotReachTheNetwork
344+
// refuses those - so the two guards together say the whole of it.
345+
func TestTheDonateButtonOpensTheSupportPage(t *testing.T) {
346+
host := &fakeHost{}
347+
window.Open(host)
348+
349+
donate := buttonNamed(host.content, text.ButtonDonate)
350+
if donate == nil {
351+
t.Fatalf("there is no %q button in the window. It carries: %v",
352+
text.ButtonDonate, buttonNames(host.content))
353+
}
354+
if donate.Disabled() {
355+
t.Fatal("the Donate button is disabled, so nobody can give anything")
356+
}
357+
358+
if host.openedCount != 0 {
359+
t.Fatalf("something opened %q before anybody pressed anything", host.opened)
360+
}
361+
362+
donate.OnTapped()
363+
364+
if host.openedCount != 1 {
365+
t.Fatalf("pressing Donate asked the window to open %d addresses", host.openedCount)
366+
}
367+
if host.opened != text.SupportURL {
368+
t.Errorf("Donate goes to %q and the support page is %q", host.opened, text.SupportURL)
369+
}
370+
}
371+
372+
// The address the button carries is the one in the sponsor file GitHub reads.
373+
//
374+
// Two places name where the money goes: this program and .github/FUNDING.yml.
375+
// They are read by different people at different times and nothing links them,
376+
// so the day one moves the other keeps pointing at the old page - and the failure
377+
// is somebody arriving somewhere that no longer takes payments, which nobody here
378+
// would ever see.
379+
func TestTheSponsorFileAndTheWindowAgreeWhereTheMoneyGoes(t *testing.T) {
380+
raw, err := os.ReadFile(filepath.Join("..", "..", ".github", "FUNDING.yml"))
381+
if err != nil {
382+
// A fresh clone of a source archive has no .github directory, and this
383+
// guard reads a document rather than the code - so it says so and stops
384+
// rather than failing for a reason that is not a defect.
385+
t.Skipf("no sponsor file to compare against: %v", err)
386+
}
387+
if !strings.Contains(string(raw), text.SupportURL) {
388+
t.Errorf(".github/FUNDING.yml does not name %q, so the button in the window and\n"+
389+
"the sponsor button on the repository lead to different places.\nThe file says:\n%s",
390+
text.SupportURL, raw)
391+
}
392+
}
1.01 KB
Loading
Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,82 @@
11
<canvas padded size="1100x1300">
22
<content>
3-
<widget pos="6,6" size="1088x1288" type="*container.AppTabs">
4-
<container size="1088x32">
3+
<container pos="6,6" size="1088x1288">
4+
<widget pos="0,38" size="1088x1250" type="*container.AppTabs">
55
<container size="1088x32">
6-
<widget size="98x32" type="*container.tabButton">
7-
<text alignment="center" bold pos="6,6" size="86x20">Single batch</text>
8-
</widget>
9-
<widget pos="104,0" size="63x32" type="*container.tabButton">
10-
<text alignment="center" bold pos="6,6" size="51x20">Presets</text>
11-
</widget>
12-
<widget pos="173,0" size="121x32" type="*container.tabButton">
13-
<text alignment="center" bold pos="6,6" size="109x20">Several batches</text>
14-
</widget>
15-
<widget pos="300,0" size="54x32" type="*container.tabButton">
16-
<text alignment="center" bold color="hyperlink" pos="6,6" size="42x20">About</text>
17-
</widget>
6+
<container size="1088x32">
7+
<widget size="98x32" type="*container.tabButton">
8+
<text alignment="center" bold pos="6,6" size="86x20">Single batch</text>
9+
</widget>
10+
<widget pos="104,0" size="63x32" type="*container.tabButton">
11+
<text alignment="center" bold pos="6,6" size="51x20">Presets</text>
12+
</widget>
13+
<widget pos="173,0" size="121x32" type="*container.tabButton">
14+
<text alignment="center" bold pos="6,6" size="109x20">Several batches</text>
15+
</widget>
16+
<widget pos="300,0" size="54x32" type="*container.tabButton">
17+
<text alignment="center" bold color="hyperlink" pos="6,6" size="42x20">About</text>
18+
</widget>
19+
</container>
1820
</container>
19-
</container>
20-
<rectangle fillColor="shadow" pos="0,32" size="1088x1"/>
21-
<rectangle fillColor="hyperlink" pos="300,32" radius="3" size="54x1"/>
22-
<container pos="0,38" size="1088x1250">
23-
<container pos="134,0" size="820x1250">
24-
<widget size="820x39" type="*widget.Label">
25-
<widget size="820x39" type="*widget.RichText">
26-
<text bold pos="6,6" size="280x27" textSize="20">Testing Files Generator 0.1.0</text>
21+
<rectangle fillColor="shadow" pos="0,32" size="1088x1"/>
22+
<rectangle fillColor="hyperlink" pos="300,32" radius="3" size="54x1"/>
23+
<container pos="0,38" size="1088x1212">
24+
<container pos="134,0" size="820x1212">
25+
<widget size="820x39" type="*widget.Label">
26+
<widget size="820x39" type="*widget.RichText">
27+
<text bold pos="6,6" size="280x27" textSize="20">Testing Files Generator 0.1.0</text>
28+
</widget>
2729
</widget>
28-
</widget>
29-
<widget pos="0,45" size="820x31" type="*widget.Label">
30-
<widget size="820x31" type="*widget.RichText">
31-
<text pos="6,6" size="513x19">Generate test files, and know how the system under test should react to them.</text>
30+
<widget pos="0,45" size="820x31" type="*widget.Label">
31+
<widget size="820x31" type="*widget.RichText">
32+
<text pos="6,6" size="513x19">Generate test files, and know how the system under test should react to them.</text>
33+
</widget>
3234
</widget>
33-
</widget>
34-
<container pos="0,82" size="820x370">
35-
<rectangle fillColor="rgba(38,38,40,255)" radius="8" size="820x370" strokeColor="separator" strokeWidth="1"/>
36-
<container size="820x370">
37-
<container pos="6,6" size="808x358">
38-
<widget size="808x35" type="*widget.Label">
39-
<widget size="808x35" type="*widget.RichText">
40-
<text bold pos="6,6" size="63x23" textSize="17">Licence</text>
35+
<container pos="0,82" size="820x370">
36+
<rectangle fillColor="rgba(38,38,40,255)" radius="8" size="820x370" strokeColor="separator" strokeWidth="1"/>
37+
<container size="820x370">
38+
<container pos="6,6" size="808x358">
39+
<widget size="808x35" type="*widget.Label">
40+
<widget size="808x35" type="*widget.RichText">
41+
<text bold pos="6,6" size="63x23" textSize="17">Licence</text>
42+
</widget>
4143
</widget>
42-
</widget>
43-
<widget pos="0,41" size="808x317" type="*widget.Label">
44-
<widget size="808x317" type="*widget.RichText">
45-
<text pos="6,6" size="150x19">Testing Files Generator</text>
46-
<text pos="6,25" size="209x19">Copyright (C) 2026 DonislawDev</text>
47-
<text pos="6,44" size="0x19"></text>
48-
<text pos="6,63" size="495x19">Released under the GNU General Public License, version 3. The full text is in</text>
49-
<text pos="6,82" size="227x19">the LICENSE file beside the source.</text>
50-
<text pos="6,101" size="0x19"></text>
51-
<text pos="6,120" size="325x19">There is no warranty, to the extent the law allows.</text>
52-
<text pos="6,139" size="0x19"></text>
53-
<text pos="6,158" size="494x19">The files you generate are yours. Generated files, recipes and manifests are</text>
54-
<text pos="6,177" size="490x19">output of this program and not derived works of it, so this licence does not</text>
55-
<text pos="6,196" size="498x19">reach them. You can generate fixtures, commit them and ship them inside a</text>
56-
<text pos="6,215" size="347x19">closed source product with no obligation of any kind.</text>
57-
<text pos="6,234" size="0x19"></text>
58-
<text pos="6,254" size="486x19">Code from other projects is compiled into this program. Their licences and</text>
59-
<text pos="6,273" size="336x19">copyright notices are in THIRD-PARTY-NOTICES.md.</text>
60-
<text pos="6,292" size="0x19"></text>
44+
<widget pos="0,41" size="808x317" type="*widget.Label">
45+
<widget size="808x317" type="*widget.RichText">
46+
<text pos="6,6" size="150x19">Testing Files Generator</text>
47+
<text pos="6,25" size="209x19">Copyright (C) 2026 DonislawDev</text>
48+
<text pos="6,44" size="0x19"></text>
49+
<text pos="6,63" size="495x19">Released under the GNU General Public License, version 3. The full text is in</text>
50+
<text pos="6,82" size="227x19">the LICENSE file beside the source.</text>
51+
<text pos="6,101" size="0x19"></text>
52+
<text pos="6,120" size="325x19">There is no warranty, to the extent the law allows.</text>
53+
<text pos="6,139" size="0x19"></text>
54+
<text pos="6,158" size="494x19">The files you generate are yours. Generated files, recipes and manifests are</text>
55+
<text pos="6,177" size="490x19">output of this program and not derived works of it, so this licence does not</text>
56+
<text pos="6,196" size="498x19">reach them. You can generate fixtures, commit them and ship them inside a</text>
57+
<text pos="6,215" size="347x19">closed source product with no obligation of any kind.</text>
58+
<text pos="6,234" size="0x19"></text>
59+
<text pos="6,254" size="486x19">Code from other projects is compiled into this program. Their licences and</text>
60+
<text pos="6,273" size="336x19">copyright notices are in THIRD-PARTY-NOTICES.md.</text>
61+
<text pos="6,292" size="0x19"></text>
62+
</widget>
6163
</widget>
62-
</widget>
64+
</container>
6365
</container>
6466
</container>
6567
</container>
6668
</container>
69+
</widget>
70+
<container size="1088x32">
71+
<spacer size="1024x32"/>
72+
<widget pos="1024,0" size="63x32" type="*widget.Button">
73+
<rectangle fillColor="button" radius="5" size="63x32"/>
74+
<rectangle size="63x32"/>
75+
<widget pos="6,6" size="51x19" type="*widget.RichText">
76+
<text alignment="center" bold size="51x19">Donate</text>
77+
</widget>
78+
</widget>
6779
</container>
68-
</widget>
80+
</container>
6981
</content>
7082
</canvas>
1.27 KB
Loading

0 commit comments

Comments
 (0)