diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c29406..3bf34e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -299,6 +299,16 @@ because it turns other people's test suites red. ### Fixed +- **The About screen now shows the support address, so the Donate button is no + longer the only way to reach it.** Pressing Donate asks your desktop to open + the page. On a machine with no browser registered that quietly does nothing, + which was the intended behaviour - a message about a button somebody pressed + out of curiosity is worse than none - except that the address itself appeared + on no screen at all, so there was nothing to fall back to. + + It is on the About screen now, under Support, on a line of its own so it can + be copied in one go. + - **A manifest too big for this build to read back now says so in the report a script reads.** A run of 25 000 files writes a manifest of about 25.9 MB against a 16 MB reading limit, so `tfg verify` and `tfg cleanup` both refuse diff --git a/internal/format/opc/opc.go b/internal/format/opc/opc.go index 097b2e0..eb19d4e 100644 --- a/internal/format/opc/opc.go +++ b/internal/format/opc/opc.go @@ -220,22 +220,6 @@ func Settle(parts []Part, shape Shape, want int64) (Package, error) { } } -// Size is what a settled package will weigh. Padding is counted rather than -// written, so asking about a package of many gigabytes costs nothing. -func Size(p Package) (int64, error) { - payload := p.Filler - if payload > 0 { - p.Filler = 0 - } else { - payload = 0 - } - n, err := size(p) - if err != nil { - return 0, err - } - return n + payload, nil -} - func size(p Package) (int64, error) { c := &counter{} if err := write(context.Background(), c, p, false); err != nil { diff --git a/internal/guard/crowding_test.go b/internal/guard/crowding_test.go index a21d136..13d4f9d 100644 --- a/internal/guard/crowding_test.go +++ b/internal/guard/crowding_test.go @@ -30,9 +30,22 @@ import ( // threshold picked out of the air is a guess, and a guess written into a gate // is a guess nobody can argue with later. const ( - // What counts as crowding. Three quarters of the ceiling is far enough from - // it that ordinary code does not trip the count, and close enough that - // something arriving there is on its way. + // What counts as crowding, as a NUMBER OF LINES rather than a share of the + // ceiling. Three quarters of the ceiling is where each of these started, and + // that is far enough from it that ordinary code does not trip the count and + // close enough that something arriving there is on its way. + // + // A starting point rather than a live ratio, and the difference is the whole + // point of these two numbers being absolute. The ceilings have come down + // since - 550 to 433 and 80 to 75 - and these have not followed, so the + // bands are tighter than three quarters today and the counts below were + // re-measured each time. That is the safe direction and it is deliberate. + // + // Do not "restore" the ratio. It was measured on 2026-08-27 and refused: + // re-deriving the band from a lowered ceiling moves it from 413 to 378, + // which pulls in seven files that did nothing and forces the cap from 2 to + // 9 - a cap going UP, which is the one direction these numbers may not go. + // A band that reshapes itself under the thing it is watching says nothing. crowdingFileLines = 413 crowdingFunctionLines = 60 diff --git a/internal/guard/supportaddress_test.go b/internal/guard/supportaddress_test.go new file mode 100644 index 0000000..b1039f9 --- /dev/null +++ b/internal/guard/supportaddress_test.go @@ -0,0 +1,61 @@ +package guard + +import ( + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" + "github.com/donislawdev/TestingFilesGenerator/internal/gui/window" +) + +// The address the Donate button leads to is also READABLE, in words, on a +// screen. +// +// This is not decoration and it is not a second way to say the button works. +// Handing an address to the desktop can fail - a machine with no browser +// registered is the ordinary case - and internal/gui swallows that refusal on +// purpose, with the reason written above OpenLink: there is nothing useful to +// say to somebody who pressed a button out of curiosity, and no harm done, +// because the address is on the About screen for anybody who wants to type it. +// +// That last clause was FALSE from the day the button was written until +// 2026-09-07. text.SupportURL had exactly one use in the whole tree, as the +// button's destination, so it appeared on no screen at all: a person whose +// desktop did nothing pressed Donate, saw nothing happen, and had nowhere to +// go. The decision to stay quiet was sound and rested on a fallback nobody had +// built. Found by tools/probes/guardonly, which reports exported definitions +// nothing names, not by reading the code. +// +// So the silence and the readable address are ONE decision, and this guard is +// what keeps them together. Delete the Support section and this goes red, which +// is the point: taking the address off the screen turns a considered silence +// back into a dead end. +func TestTheSupportAddressIsReadableOnAScreen(t *testing.T) { + shown := textIn(window.About(newFakeHost(t))) + + if !strings.Contains(shown, text.SupportURL) { + t.Errorf("the About screen does not carry %q, so somebody whose desktop cannot open\n"+ + "the Donate button has no way to reach the page - and internal/gui stays quiet\n"+ + "about that failure precisely because this screen is supposed to carry it.\n"+ + "The screen says:\n%s", text.SupportURL, shown) + } +} + +// The address is on the screen as its own line rather than buried in a +// sentence. +// +// Somebody typing it into a browser by hand is exactly the reader this exists +// for, and an address wrapped inside a paragraph is one they have to pick out +// of prose. Asked as "a line that is the address and nothing else", which is +// the shape a person can select in one gesture. +func TestTheSupportAddressStandsOnItsOwnLine(t *testing.T) { + shown := textIn(window.About(newFakeHost(t))) + + for _, line := range strings.Split(shown, "\n") { + if strings.TrimSpace(line) == text.SupportURL { + return + } + } + t.Errorf("the About screen names %q only inside a longer line, so somebody copying it\n"+ + "has to pick it out of a sentence. The screen says:\n%s", text.SupportURL, shown) +} diff --git a/internal/guard/testdata/screens/about.png b/internal/guard/testdata/screens/about.png index 7c6dd7a..ac6da47 100644 Binary files a/internal/guard/testdata/screens/about.png and b/internal/guard/testdata/screens/about.png differ diff --git a/internal/guard/testdata/screens/about.xml b/internal/guard/testdata/screens/about.xml index 408a651..e08f617 100644 --- a/internal/guard/testdata/screens/about.xml +++ b/internal/guard/testdata/screens/about.xml @@ -25,8 +25,8 @@ - - + + @@ -73,7 +73,29 @@ - + + + + + + + Support + + + + + Opens the support page in your browser. The tool is free and stays free - this pays for the time that goes into it. + + + + + https://donislawdev.com/support/ + + + + + + @@ -121,7 +143,7 @@ - + @@ -151,8 +173,8 @@ - - + + diff --git a/internal/gui/parts/parts.go b/internal/gui/parts/parts.go index 508d984..9766385 100644 --- a/internal/gui/parts/parts.go +++ b/internal/gui/parts/parts.go @@ -491,10 +491,6 @@ const ( GapSection = 14 ) -// SectionGap is the older name for GapSection, kept because that is the name -// the recipe screen and the guards were written against. -const SectionGap = GapSection - // Column stacks its children with one fixed gap, whatever the theme's padding // is. // diff --git a/internal/gui/run_cgo.go b/internal/gui/run_cgo.go index d8d2e1a..67b7730 100644 --- a/internal/gui/run_cgo.go +++ b/internal/gui/run_cgo.go @@ -158,6 +158,14 @@ func (d desktop) ChooseDirectory(chosen func(string)) { // somebody whose desktop has no browser registered, no screen to say it on that // would not be a modal about a button they pressed by curiosity, and no harm // done - the address is in the About screen for anybody who wants to type it. +// +// That last clause was FALSE until 2026-09-07 and is the reason this paragraph +// is worth reading twice. text.SupportURL had exactly one use in the whole +// tree, as this button's destination, so it was on no screen at all and a +// person whose desktop did nothing had nowhere to go. The decision to stay +// quiet was sound and rested on a fallback that did not exist. The About screen +// carries the address now, in its Support section, which is what makes the +// sentence above true rather than merely reasonable. func (d desktop) OpenLink(address string) { parsed, err := url.Parse(address) if err != nil { diff --git a/internal/gui/text/locale/en.json b/internal/gui/text/locale/en.json index aac4043..3b8d654 100644 --- a/internal/gui/text/locale/en.json +++ b/internal/gui/text/locale/en.json @@ -288,10 +288,6 @@ "description": "Shown in the window.", "other": "Nothing was written. Check the settings marked above." }, - "SectionBatches": { - "description": "The heading over a group of fields.", - "other": "Batches" - }, "SectionCarriedCode": { "description": "The heading over a group of fields.", "other": "Third party code compiled in" @@ -324,6 +320,10 @@ "description": "The heading over a group of fields.", "other": "Settings" }, + "SectionSupport": { + "description": "The heading over a group of fields.", + "other": "Support" + }, "SettingKey": { "description": "Shown in the window. Carries one value, {{.Key}}, which has to stay spelled exactly that way.", "other": "Written as {{.Key}} in a recipe." @@ -376,10 +376,6 @@ "description": "Shown in the window.", "other": "Working out what this would cost..." }, - "WritingFiles": { - "description": "Shown in the window. Carries one value, {{.Files}}, which has to stay spelled exactly that way.", - "other": "Writing {{.Files}}..." - }, "WritingTo": { "description": "Shown in the window. Carries one value, {{.Directory}}, which has to stay spelled exactly that way.", "other": "Files will go to {{.Directory}}" diff --git a/internal/gui/text/screens.go b/internal/gui/text/screens.go index 9eb359a..97e66e4 100644 --- a/internal/gui/text/screens.go +++ b/internal/gui/text/screens.go @@ -56,6 +56,7 @@ func SectionOutput() string { return say("SectionOutput", "Output") } func SectionPreset() string { return say("SectionPreset", "The question") } func SectionSettings() string { return say("SectionSettings", "Settings") } func SectionLicence() string { return say("SectionLicence", "Licence") } +func SectionSupport() string { return say("SectionSupport", "Support") } // The two headings over what this binary carries that somebody else wrote. // @@ -280,9 +281,8 @@ func TooManyFiles(count int64, reason error) string { // difference between the two screens is how many batches, not how advanced // the person is. "Advanced" would have said the other screen is for // beginners, which is not true of anybody generating one batch of files. -func HeadingRecipe() string { return say("HeadingRecipe", "Run several batches together") } -func TabRecipe() string { return say("TabRecipe", "Several batches") } -func SectionBatches() string { return say("SectionBatches", "Batches") } +func HeadingRecipe() string { return say("HeadingRecipe", "Run several batches together") } +func TabRecipe() string { return say("TabRecipe", "Several batches") } // BatchHeading names one batch in the list, counted the way the refusals count. // diff --git a/internal/gui/text/text.go b/internal/gui/text/text.go index c6e5721..597f319 100644 --- a/internal/gui/text/text.go +++ b/internal/gui/text/text.go @@ -115,12 +115,6 @@ func WritingTo(dir string) string { return sayf("WritingTo", "Files will go to {{.Directory}}", map[string]any{"Directory": dir}) } -// WritingFiles is the line under the bar at the moment a run starts, before -// the first progress report arrives. -func WritingFiles(count int) string { - return sayf("WritingFiles", "Writing {{.Files}}...", map[string]any{"Files": files(count)}) -} - // WorkingOutTheCost is what a preview says while it is going. // // A preview does disk work - it asks how much room there is and whether any of diff --git a/internal/gui/window/about.go b/internal/gui/window/about.go index f2ca5c1..9dbc554 100644 --- a/internal/gui/window/about.go +++ b/internal/gui/window/about.go @@ -87,6 +87,20 @@ func About(h Host) fyne.CanvasObject { // as a page of the application rather than as the one screen that was // left as it was. parts.Section(text.SectionLicence(), parts.Prose(version.LicenceNotice)), + // The support address, in words, on the one screen somebody reads when + // they are deciding what this program costs them. + // + // It is here because the Donate button cannot always work and says so: + // handing an address to the desktop fails on a machine with no browser + // registered, and that refusal is swallowed on purpose, because there is + // no useful thing to say to somebody who pressed a button out of + // curiosity. That decision only holds while the address is READABLE + // somewhere, and until 2026-09-07 it was not - the constant had exactly + // one use in the whole tree, as the button's destination, so a person + // whose desktop did nothing had nowhere to go. The comment beside + // OpenLink said this screen carried it. This is that screen carrying it. + parts.Section(text.SectionSupport(), + parts.Prose(text.DetailDonate()), parts.Prose(text.SupportURL)), } sections = append(sections, carried()...) page := parts.Screen(text.HeadingAbout(version.Version), sections...) diff --git a/internal/recipe/compose.go b/internal/recipe/compose.go index 0dd7bfe..71f8de2 100644 --- a/internal/recipe/compose.go +++ b/internal/recipe/compose.go @@ -308,6 +308,17 @@ func firstControl(value string) (rune, bool) { // name for a given box: both sides can agree and both be wrong. That is what // TestEveryRefusalAboutABatchMarksThatBatchsBox is for, and the two together are // what make the pairing safe rather than merely consistent. +// +// Eleven of these have no caller today and every one of them stays. This is a +// VOCABULARY rather than a list of things in use: it names the keys the recipe +// schema has, and half a vocabulary is the "format added halfway" shape that +// the exhaustive linter is switched on to catch one axis over. Five of the +// eleven - locale, policy, engine, extends, with - name settings this build +// refuses out loud with "not in this build yet", so a set holding only the keys +// that happen to have a consumer would be missing exactly the ones a refusal +// has to be able to name. Measured 2026-09-07 by tools/probes/guardonly, which +// is where the eleven came from, and written here so the next scan does not +// reach for the delete. const ( KeyTargets = core.KeyTargets KeyVersion = "version"