Skip to content

Commit fb4a99a

Browse files
donislawdevclaude
andcommitted
gui: say where the files go, and let a declaration describe itself
Three fixes, all of them from the owner looking at the screen rather than from a guard. That step has now found something every time it has been taken. The output directory is written out in full instead of as a dot, with a Choose button beside it. A dot is clear in a terminal, where you walked into the directory yourself. Started from a desktop it means somewhere, and this is the one part of the tool that writes into other people's directories. The box stays a box, so a path somebody sent you can still be pasted in. A text setting can say what sort of text it takes. "text" under a field is a word where a description should be, and the declaration for the spread of a boundary set already knew it wanted a list of sizes with nowhere to put it. The guard for that found two more the same way, in zip and targz. What a preset typically finds is a list of lines. Run together it read as three times as many findings as there are, because the entries have commas in them. The picker cost one module and which one is the point. fyne.io/fyne/v2/dialog imports github.com/FyshOS/fancyfs, which was already named in our module graph because the toolkit requires it - importing dialog moved it from named to downloaded, checksummed and compiled in. Checked before accepting: BSD-3, one way compatible with GPL-3.0 like the eleven other BSD-3 modules here, 129 lines, written by the author of the toolkit itself, and reached from a single line where it decorates folder icons. Tidying afterwards pruned kr/text, which took creack/pty out of the graph, so the dependency gate went from 69 modules to 68 and its expected list was changed deliberately rather than to make it pass. That import also showed a hole in the notices guard. It asked whether every listed module is still built and never the other way round, so a module that ARRIVED went unlisted - which is the direction the licences care about, since they require the notice to travel with the code that ships. It asks both now. Its first run raised two false alarms worth recording: goccy/go-yaml and x/text are noticed under their own headings with the whole licence text rather than as table rows, which is a stronger notice and not a weaker one. Host gained ChooseDirectory and it always calls back, including with nothing when somebody cancels. A host that stays silent on cancel leaves the caller's handling of that answer unreachable from any test, which is how the mutation for it first came back NOT CAUGHT. Guards 247 to 252, mutations 229 to 235. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d297357 commit fb4a99a

16 files changed

Lines changed: 395 additions & 41 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,21 @@ jobs:
6161
# to be visible rather than discovered later.
6262
#
6363
# Two questions rather than one, since the graphics toolkit arrived
64-
# on 2026-08-05. The graph below is 69 modules and most of them are
64+
# on 2026-08-05. The graph below is 68 modules and most of them are
6565
# Fyne's. Their licences were measured before it was added and every
6666
# one in the linked set is permissive and one way compatible with
6767
# GPL-3.0: 13 BSD-3, 10 MIT, 1 Apache-2.0, 1 BSD-2, 1 ISC.
6868
#
69+
# Changed once since, on 2026-08-05, when the window gained a
70+
# folder picker. github.com/FyshOS/fancyfs was already named here
71+
# because the toolkit requires it - importing the dialog package
72+
# moved it from named to downloaded, checksummed and compiled in.
73+
# Checked before it was accepted: BSD-3, 129 lines, written by the
74+
# toolkit's own author, and reached from one line of the dialog
75+
# package where it decorates folder icons. Tidying after it pruned
76+
# github.com/kr/text from go.mod, which took github.com/creack/pty
77+
# out of the graph, taking the count from 69 to 68.
78+
#
6979
# The second question protects what ships to most people. The
7080
# command line binary links exactly two external modules and the
7181
# toolkit is not among them, so a build for a server carries no
@@ -86,7 +96,6 @@ jobs:
8696
github.com/anthonynsimon/bild \
8797
github.com/clipperhouse/uax29/v2 \
8898
github.com/cpuguy83/go-md2man/v2 \
89-
github.com/creack/pty \
9099
github.com/davecgh/go-spew \
91100
github.com/felixge/fgprof \
92101
github.com/fogleman/gg \

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ because it turns other people's test suites red.
1616

1717
### Added
1818

19+
- **The window says where the files will go, and lets you browse to it.** The
20+
output directory is written out in full instead of as a dot, and there is a
21+
Choose button beside it. A dot is clear in a terminal, where you walked into
22+
the directory yourself. Started from a desktop it means somewhere, and this is
23+
the part of the tool that writes into your directories.
24+
The box is still a box, so a path somebody sent you can be pasted straight in.
25+
1926
- **Presets in the window.** A second screen that starts from a question rather
2027
than from numbers: pick what you are testing, and the set is worked out from
2128
the answer. It shows the question the preset closes and the mistakes it

THIRD-PARTY-NOTICES.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,25 @@ Source: <https://go.googlesource.com/text>
119119

120120
## The window binary only
121121

122-
These 25 modules are the graphics toolkit and what it brings with it. They are
122+
These 26 modules are the graphics toolkit and what it brings with it. They are
123123
in `tfg-gui` and in no other binary this project produces.
124124

125125
Their licences were read from the source of each module before the toolkit was
126126
added, on 2026-08-05. All are permissive and one way compatible with GPL-3.0:
127-
eleven BSD 3-Clause, nine MIT, one Apache-2.0, one BSD 2-Clause and one ISC.
127+
twelve BSD 3-Clause, nine MIT, one Apache-2.0, one BSD 2-Clause and one ISC.
128128
None is GPL-2.0-only or LGPL, which would not have been compatible.
129129

130+
One arrived later than the rest. `github.com/FyshOS/fancyfs` came in on the
131+
same day with the folder picker, because the toolkit's dialog package
132+
imports it to decorate folder icons. Its licence was read before it was
133+
accepted, the same as the others.
134+
130135
| module | version | licence | copyright |
131136
|---|---|---|---|
132137
| `fyne.io/fyne/v2` | v2.8.0 | BSD-3-Clause | (C) 2018 Fyne.io developers (see AUTHORS) |
133138
| `fyne.io/systray` | v1.12.2 | Apache-2.0 | 2014 Brave New Software Project, Inc. |
134139
| `github.com/BurntSushi/toml` | v1.6.0 | MIT | (c) 2013 TOML authors |
140+
| `github.com/FyshOS/fancyfs` | v0.0.1 | BSD-3-Clause | (C) 2025 FyshOS developers (see AUTHORS) |
135141
| `github.com/anthonynsimon/bild` | v0.14.0 | MIT | (c) 2021 Anthony Najjar Simon |
136142
| `github.com/clipperhouse/uax29/v2` | v2.2.0 | MIT | (c) 2020 Matt Sherman |
137143
| `github.com/fredbi/uri` | v1.1.1 | MIT | (c) 2018 Frederic Bidon |

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ require github.com/goccy/go-yaml v1.19.2
1010

1111
require golang.org/x/text v0.40.0
1212

13+
require github.com/FyshOS/fancyfs v0.0.1 // indirect
14+
1315
require (
1416
fyne.io/fyne/v2 v2.8.0
1517
fyne.io/systray v1.12.2 // indirect
@@ -32,7 +34,6 @@ require (
3234
github.com/hack-pad/safejs v0.1.0 // indirect
3335
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
3436
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
35-
github.com/kr/text v0.2.0 // indirect
3637
github.com/mattn/go-runewidth v0.0.24 // indirect
3738
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
3839
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect

go.sum

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
44
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
55
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
66
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
7+
github.com/FyshOS/fancyfs v0.0.1 h1:kgvm7VvwOMLkYTqSflplp62SlMVWQ2uAoHw9CXwXHYg=
8+
github.com/FyshOS/fancyfs v0.0.1/go.mod h1:S5SHVz/5R72iCXOxCqdcyTPSlg3JxNd0gaHyGBSrY8A=
79
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
810
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
911
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
1012
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
11-
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1213
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1314
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1415
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=

internal/format/format.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ type Property struct {
147147
// Choices are the allowed values of a choice, lower case.
148148
Choices []string
149149

150+
// Shape is what free text has to look like, in a few words, for a kind that
151+
// has no range and no closed set to describe itself with.
152+
//
153+
// It exists because "text" was the whole of what a text setting could say
154+
// about itself, and under a field that reads as no description at all -
155+
// seen on screen on 2026-08-05, where the spread of a boundary set was
156+
// announced as "text, default 1B,1kb,1mb". The value it wants is a list of
157+
// sizes separated by commas, the declaration knew that, and there was
158+
// nowhere to put it. Ignored by every other kind, which say what they take
159+
// from their own range or set.
160+
Shape string
161+
150162
// Default is what the format uses when nothing says otherwise, written
151163
// the way a person would write it. Empty means the format works it out -
152164
// a picture size chosen to fit the requested bytes, for instance.
@@ -421,7 +433,12 @@ func (p Property) Allowed() string {
421433
case PropertySize:
422434
what = "a size such as 2mb, or a plain byte count"
423435
default:
424-
what = "text"
436+
// A text setting describes itself with Shape or not at all. Saying
437+
// "text" under a field is a word where a description should be.
438+
what = p.Shape
439+
}
440+
if p.Default != "" && what == "" {
441+
return "default " + p.Default
425442
}
426443
if p.Default != "" {
427444
what += ", default " + p.Default

internal/format/targz/targz.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func init() {
137137
},
138138
{
139139
Name: "entry_format", Kind: format.PropertyText,
140+
Shape: "the id of a format, as tfg formats lists them",
140141
// Not a choice, for the same reason as in ZIP: the allowed
141142
// values are whatever this build registered, and a list frozen
142143
// here would drift the moment a format is added.

internal/format/zip/zip.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func init() {
107107
},
108108
{
109109
Name: "entry_format", Kind: format.PropertyText,
110+
Shape: "the id of a format, as tfg formats lists them",
110111
// Not a choice, because the allowed values are whatever this
111112
// build registered, and a list frozen here would drift away
112113
// from the registry the moment a format is added.

internal/guard/notices_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os/exec"
66
"path/filepath"
77
"regexp"
8+
"sort"
89
"strings"
910
"testing"
1011
)
@@ -27,6 +28,35 @@ import (
2728
// is a licence question and belongs to a person reading each licence, and a
2829
// guard that added rows automatically would defeat the reason the file exists.
2930

31+
// unlisted is every module the binary carries that the notices do not name.
32+
//
33+
// The standard library is not a module and does not appear in the build
34+
// list, so nothing has to be excluded here - what comes back is third party
35+
// code that ships.
36+
func unlisted(body string, built map[string]string) []string {
37+
// Two ways of being named, because this file uses both. Most modules get
38+
// a row in a table. The two that carry the command line get a section of
39+
// their own with the whole licence text under a heading, which is a
40+
// stronger notice rather than a weaker one - reading only the rows called
41+
// them missing the first time this ran.
42+
named := map[string]bool{}
43+
for _, line := range strings.Split(body, "\n") {
44+
if m := noticeRow.FindStringSubmatch(line); m != nil {
45+
named[m[1]] = true
46+
}
47+
if heading, found := strings.CutPrefix(line, "## "); found {
48+
named[strings.TrimSpace(heading)] = true
49+
}
50+
}
51+
var missing []string
52+
for path := range built {
53+
if !named[path] {
54+
missing = append(missing, path)
55+
}
56+
}
57+
return missing
58+
}
59+
3060
var noticeRow = regexp.MustCompile(`^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \| (v[^ |]+) \|`)
3161

3262
func TestTheNoticesFileNamesTheVersionsThatAreActuallyBuilt(t *testing.T) {
@@ -65,6 +95,19 @@ func TestTheNoticesFileNamesTheVersionsThatAreActuallyBuilt(t *testing.T) {
6595
if listed < 5 {
6696
t.Fatalf("only %d rows were compared, so this guard would pass on an empty table", listed)
6797
}
98+
99+
// And the other direction, which is the one that was missing. Until
100+
// 2026-08-05 this only asked whether every listed module is still built,
101+
// so a module that ARRIVED went unlisted and unnoticed - and it was the
102+
// folder picker arriving that showed it. Their licences require the
103+
// notice to travel with the code that ships, and an absent notice fails
104+
// that in a way a wrong version number does not.
105+
if missing := unlisted(string(body), built); len(missing) > 0 {
106+
sort.Strings(missing)
107+
t.Errorf("%d module(s) are compiled into the window and named nowhere in the notices:\n %s\n"+
108+
"Read the licence out of the module's own source, add a row, and change the counts "+
109+
"in the paragraph above the table.", len(missing), strings.Join(missing, "\n "))
110+
}
68111
t.Logf("%d module version(s) in the notices agree with what the window binary links", listed)
69112
}
70113

internal/guard/presetwindow_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"fyne.io/fyne/v2/widget"
1313

1414
"github.com/donislawdev/TestingFilesGenerator/internal/cli"
15+
"github.com/donislawdev/TestingFilesGenerator/internal/format"
16+
"github.com/donislawdev/TestingFilesGenerator/internal/gui/window"
1517
"github.com/donislawdev/TestingFilesGenerator/internal/preset"
1618
)
1719

@@ -253,3 +255,144 @@ func hashLine(recorded string) string {
253255
}
254256
return ""
255257
}
258+
259+
// Where the files will land is legible before the button is pressed.
260+
//
261+
// In a terminal a dot is the directory you typed your way into. A window
262+
// started from a desktop has a working directory nobody chose and nobody can
263+
// see, so the same dot means "somewhere" - and this is the one part of this
264+
// tool that writes into other people's directories. The destination is
265+
// unchanged, what changed is that it can be read.
266+
func TestBothScreensSayWhereTheFilesWillGo(t *testing.T) {
267+
host, generate := screen(t)
268+
press(t, generate, "Presets")
269+
270+
for name, content := range map[string]fyne.CanvasObject{
271+
"the generate screen": generate,
272+
"the preset screen": host.content,
273+
} {
274+
shown := entryUnder(t, content, "output directory").Text
275+
if shown == "." || shown == "" {
276+
t.Errorf("%s offers %q as the output directory, which says nothing about where the files go",
277+
name, shown)
278+
continue
279+
}
280+
if !filepath.IsAbs(shown) {
281+
t.Errorf("%s offers %q, which is not a path somebody can read off the screen", name, shown)
282+
}
283+
}
284+
}
285+
286+
// A text setting says what sort of text it takes.
287+
//
288+
// "text" was the whole of what a text setting could say about itself, and under
289+
// a field that reads as no description at all - seen on screen on 2026-08-05 as
290+
// "text, default 1B,1kb,1mb", where the value wanted is a list of sizes. The
291+
// declaration knew that and had nowhere to put it.
292+
func TestNoTextSettingDescribesItselfAsText(t *testing.T) {
293+
checked := 0
294+
for _, p := range preset.All() {
295+
for _, param := range p.Parameters {
296+
if param.Kind != format.PropertyText {
297+
continue
298+
}
299+
checked++
300+
if strings.HasPrefix(param.Allowed(), "text") {
301+
t.Errorf("%s.%s announces itself as %q, which is a word where a description should be",
302+
p.ID, param.Name, param.Allowed())
303+
}
304+
if param.Shape == "" {
305+
t.Errorf("%s.%s is free text and declares no shape, so nothing can say what it takes",
306+
p.ID, param.Name)
307+
}
308+
}
309+
}
310+
for _, d := range format.All() {
311+
for _, param := range d.Properties {
312+
if param.Kind == format.PropertyText && param.Shape == "" {
313+
t.Errorf("%s.%s is free text and declares no shape", d.ID, param.Name)
314+
}
315+
}
316+
}
317+
if checked == 0 {
318+
t.Skip("no text setting is declared in this build")
319+
}
320+
t.Logf("%d text setting(s), each declaring what it takes", checked)
321+
}
322+
323+
// What a preset typically finds is a list, not a sentence.
324+
//
325+
// The entries have commas inside them, so run together they read as three times
326+
// as many items as there are. Only visible by looking at the screen.
327+
func TestWhatAPresetFindsIsShownAsSeparateLines(t *testing.T) {
328+
_, content := presetScreen(t)
329+
picker := controlUnder(content, "preset").(*widget.Select)
330+
331+
for _, p := range preset.All() {
332+
if len(p.Catches) < 2 {
333+
continue
334+
}
335+
picker.SetSelected(p.ID)
336+
shown := textIn(content)
337+
for _, catch := range p.Catches {
338+
if !strings.Contains(shown, "\n"+" - "+catch+"\n") &&
339+
!strings.Contains(shown, " - "+catch) {
340+
t.Errorf("%s: %q is not on a line of its own", p.ID, catch)
341+
}
342+
}
343+
// Joined into one sentence they would share a line with each other.
344+
if strings.Contains(shown, p.Catches[0]+" and "+p.Catches[1]) ||
345+
strings.Contains(shown, p.Catches[0]+", "+p.Catches[1]) {
346+
t.Errorf("%s runs its findings together into one sentence", p.ID)
347+
}
348+
}
349+
}
350+
351+
// The browse button reaches the window and its answer lands in the field.
352+
//
353+
// A picker needs a real window, so what is provable here is the wiring rather
354+
// than the dialog: that the button asks, and that what comes back is what the
355+
// run will use. A button that asks and drops the answer looks exactly like one
356+
// that works, which is the shape this project keeps meeting.
357+
func TestBrowsingForADirectoryPutsItInTheField(t *testing.T) {
358+
host := &fakeHost{picked: filepath.Join(t.TempDir(), "chosen")}
359+
window.Open(host)
360+
361+
for _, screenName := range []string{"generate", "preset"} {
362+
if screenName == "preset" {
363+
press(t, host.content, "Presets")
364+
}
365+
content := host.content
366+
367+
before := entryUnder(t, content, "output directory").Text
368+
press(t, content, "Choose...")
369+
370+
if host.asked == 0 {
371+
t.Fatalf("the %s screen has a browse button that asks nobody", screenName)
372+
}
373+
after := entryUnder(t, content, "output directory").Text
374+
if after == before {
375+
t.Errorf("the %s screen dropped the directory that was chosen, so the button does nothing",
376+
screenName)
377+
}
378+
if after != host.picked {
379+
t.Errorf("the %s screen holds %q and %q was chosen", screenName, after, host.picked)
380+
}
381+
}
382+
}
383+
384+
// Changing your mind leaves the field alone.
385+
//
386+
// A picker that is cancelled answers with nothing, and a field emptied by
387+
// cancelling would send the run at a directory nobody named.
388+
func TestCancellingTheDirectoryPickerLeavesTheFieldAlone(t *testing.T) {
389+
host := &fakeHost{} // picked is empty, so nothing is chosen
390+
window.Open(host)
391+
392+
before := entryUnder(t, host.content, "output directory").Text
393+
press(t, host.content, "Choose...")
394+
395+
if after := entryUnder(t, host.content, "output directory").Text; after != before {
396+
t.Errorf("cancelling the picker changed the field from %q to %q", before, after)
397+
}
398+
}

0 commit comments

Comments
 (0)