Skip to content
Closed
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
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,66 @@ because it turns other people's test suites red.

### Changed

- **The window opens at 1120 by 760 and forgets the size it remembered once.**
It opened at 1000 by 1000, which was measured for the older single column
layout where the form carried the output directory and needed 958 px of
height. Moving that directory into the column on the right took the form down
to 337 px, so the window had been opening with about 563 px of empty space
below the form.

A size you had dragged the window to is remembered, and a remembered size wins
over any default - so this release ignores the one it had stored for you, once.
The next size you leave the window at is kept as before. Nothing is removed
from the settings file.

- **The form lines up with the left edge of the window instead of sitting in the
middle of it.** With the report column pinned to the right, centring the form
in what was left put a margin on the left with nothing to answer it - at
1520 px wide the form started 152 px in from the tabs above it.

- **The buttons that start a run reach the right edge of the window.** They were
pushed to the right of the form's own column, which left them 156 px short of
the edge in a 1120 px window.

- **The panel on the right is as tall as what it has to say.** It used to take
whatever height was left over and draw it as a panel, so at 1120 by 760 it was
a 414 px box around one sentence.

- **Byte counts are grouped in threes, in the window and on the command line.**
A total used to print as `2516582400 B`. It now prints as `2 516 582 400 B`.

Grouped with a space rather than a comma because a comma means a thousands
mark in some countries and a decimal point in others, and this tool is read in
both. Machine output is untouched: `--json` and the manifest carry numbers
rather than sentences, so nothing that parses them sees any of this.

If you have a script reading a byte count out of the human output, it needs to
take the spaces out. Two of ours did, and both said the tool had broken rather
than that they had.

- **The window has been rebuilt.** Same four screens, same settings, same
results - a different shape.

The form is on the left and everything about where the files go is on the
right, in a column that does not scroll away. It carries what the run has to
say, so the answer to a press stays next to the numbers it is about instead of
being pinned to the bottom of the window. On the batch screen that column also
shows the manifest name and the seed, which used to be below every batch.

Preview and Generate moved from the middle of the bar to its right hand end,
and each now shows the key that presses it - `Ctrl+P` and `Ctrl+Enter`, with
`Esc` on Cancel. Those keys have worked since 0.3.0-rc1 and nothing said so.

The lettering is Inter. Field names are smaller and quieter than the values in
them, section titles are smaller than they were, and every measured number -
a byte count, a total, a speed - is set in a monospaced face so digits line up.
The star marking a setting a run cannot do without is no longer red, so red on
a screen now means something was actually refused. The window opens 1120 by
760 and cannot be made narrower than its two columns need.

`About` is two columns as well, with the licence on the left and everything
the binary carries in a list on the right that scrolls on its own.

- **Notes are reported once per thing they say, not once per file.** A run of
25 000 one-byte text files used to print 25 001 `note:` lines, every one of
them the same sentence about the label not fitting. It now prints one, with
Expand Down
4 changes: 3 additions & 1 deletion THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ the font files themselves.
|---|---|---|---|
| Noto Sans, four styles | `fyne.io/fyne/v2/theme` | OFL-1.1 | Copyright 2015 Google Inc. All Rights Reserved. |
| Inter, symbols only | `fyne.io/fyne/v2/theme` | OFL-1.1 | (c) 2020 The Inter Project Authors |
| Inter, text faces, two weights | this repository, `internal/gui/typeface` | OFL-1.1 | Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) |
| DejaVu Sans Mono for Powerline | `fyne.io/fyne/v2/theme` | Bitstream-Vera | (c) 2003 Bitstream, Inc. DejaVu changes are in the public domain |
| EmojiOne Color | `fyne.io/fyne/v2/theme` | MIT, and read the note below | Copyright 2016 Adobe Systems Incorporated |
| Fyne icon set, 96 drawings and one image | `fyne.io/fyne/v2/theme` | BSD-3-Clause | (C) 2018 Fyne.io developers (see AUTHORS) |
Expand Down Expand Up @@ -633,7 +634,8 @@ and only those were replaced by their plain equivalents.

### SIL Open Font License 1.1

Applies to Noto Sans and to Inter.
Applies to Noto Sans, to the Inter symbols the toolkit carries, and to the two
Inter text faces this repository embeds itself.

```
------------------------------
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
)

Expand Down Expand Up @@ -109,8 +110,8 @@ func entryFor(d format.Descriptor) formatEntry {
// list and ignore the argument, ending with 0 - so there was no way to ask what
// a format accepts, and the silence looked like an answer.
func describeOne(d format.Descriptor, out io.Writer) {
fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %d B\n",
d.ID, d.Fidelity, d.Determinism, smallestAccepted(d))
fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %s\n",
d.ID, d.Fidelity, d.Determinism, core.ExactBytes(smallestAccepted(d)))
fmt.Fprintf(out, " extension %s\n", d.Extension)
fmt.Fprintf(out, " padding %s\n", d.Padding.Name)
fmt.Fprintf(out, " label %s\n", d.Label)
Expand Down
14 changes: 10 additions & 4 deletions internal/cli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,16 @@ func sizesFromFlags(g *generateOpts, errOut io.Writer) (sizes []int64, low, high
if errors.Is(err, core.ErrBoundaryTooSmall) {
// A number somebody typed, so this is USAGE rather than a problem
// with a document. The end above it keeps the code it had.
// Ungrouped, unlike every other count this program prints, and the
// exception is deliberate: this echoes back the number somebody
// typed after --boundary. A message that quotes your input and
// respells it on the way is a message you have to translate before
// you can compare it with what you wrote.
fmt.Fprintf(errOut, "tfg: --boundary %d B is too small - %s\n", limit, err)
return nil, 0, 0, 0, ExitUsage
}
if err != nil {
// Ungrouped for the same reason as the line above it.
fmt.Fprintf(errOut, "tfg: --boundary %d B is too large - %s\n", limit, err)
return nil, 0, 0, 0, ExitRecipe
}
Expand Down Expand Up @@ -387,9 +393,9 @@ func produce(ctx context.Context, targets []engine.Target, opt engine.Options, g
// Echo the exact byte count. The exact number is the point of this tool,
// and it is what any other tool will show when the user goes to check the
// file.
fmt.Fprintf(errOut, "%s in %s, %d B total\n",
fmt.Fprintf(errOut, "%s in %s, %s total\n",
core.Count(len(planned), "file", "files"), core.Count(len(targets), "target", "targets"),
engine.TotalBytes(planned))
core.ExactBytes(engine.TotalBytes(planned)))

echoBoundaries(targets, planned, errOut)
echoManifestReach(planned, errOut)
Expand Down Expand Up @@ -530,10 +536,10 @@ func echoBoundaries(targets []engine.Target, planned []engine.PlannedFile, errOu
if t.BoundaryLimit <= 0 {
continue
}
fmt.Fprintf(errOut, "boundary %q around %d B:\n", t.ID, t.BoundaryLimit)
fmt.Fprintf(errOut, "boundary %q around %s:\n", t.ID, core.ExactBytes(t.BoundaryLimit))
for _, f := range planned {
if f.Target == t {
fmt.Fprintf(errOut, " %-26s %d B\n", f.Name, f.Plan.Bytes)
fmt.Fprintf(errOut, " %-26s %s\n", f.Name, core.ExactBytes(f.Plan.Bytes))
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/cli/preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ func describePreset(e *preset.Expansion, b budget, out io.Writer) {
fmt.Fprintf(out, " --%-12s the global flag, this preset gives it a default\n", name)
}

fmt.Fprintf(out, "\nbudget at these values:\n %s, %s, %d B total, format %s\n",
fmt.Fprintf(out, "\nbudget at these values:\n %s, %s, %s total, format %s\n",
core.Count(b.Targets, "target", "targets"), core.Count(b.Files, "file", "files"),
b.Bytes, strings.Join(b.Formats, ", "))
core.ExactBytes(b.Bytes), strings.Join(b.Formats, ", "))
for _, note := range e.Notes() {
fmt.Fprintf(out, "\nnote: %s\n", note)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/recipecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
}, ExitOK)
}

fmt.Fprintf(out, "%s is valid: %s, %s, %d B total\n%s\n",
path, core.Count(len(rec.Targets), "target", "targets"), core.Count(len(planned), "file", "files"), engine.TotalBytes(planned), hash)
fmt.Fprintf(out, "%s is valid: %s, %s, %s total\n%s\n",
path, core.Count(len(rec.Targets), "target", "targets"), core.Count(len(planned), "file", "files"),
core.ExactBytes(engine.TotalBytes(planned)), hash)
return ExitOK
}

Expand Down
67 changes: 66 additions & 1 deletion internal/core/humanise.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package core
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)

Expand All @@ -20,7 +22,10 @@ import (
func HumanBytes(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
// Through ExactBytes rather than its own %d, so the two never spell one
// number two ways. Below 1024 there is nothing to group, which is
// exactly why this is easy to get wrong and leave wrong.
return ExactBytes(n)
}
div, exp := int64(unit), 0
for n/div >= unit && exp < 3 {
Expand All @@ -30,6 +35,66 @@ func HumanBytes(n int64) string {
return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGT"[exp])
}

// ExactBytes writes a count out in full, grouped in threes, with its unit.
//
// The exact number is the point of this tool and it can never be replaced by a
// rounded one - but eleven digits in a row is a number nobody reads, and both
// surfaces printed it that way. "2516582400 B" was measured on the window's run
// panel and on four lines of the command line, and the owner's report of it was
// that the bytes are welcome and unreadable, which are both true at once.
//
// Grouped with a space rather than a comma, and that is the one choice here
// worth writing down. A comma is the thousands mark in English and the decimal
// mark for most of Europe, so "2,516" is either two and a half thousand or two
// and a half depending on who is reading - and the people who read this run it
// in every country. A space means the same thing everywhere.
//
// Machine output is untouched on purpose. Nothing in a manifest or under --json
// goes through here, because a number there is a number and not a sentence.
func ExactBytes(n int64) string {
return Grouped(n) + " B"
}

// Grouped is a plain number, spaced the same way a byte count is.
//
// It is the same spelling without the unit, for the counts that are not bytes -
// how many files a run comes to, most of all, which reaches five digits on the
// sets this tool is built for. Split out of ExactBytes on 2026-09-08 rather
// than written again beside it, because two functions putting spaces into
// numbers is two functions that can come to disagree about where.
func Grouped(n int64) string {
return groupedInThrees(strconv.FormatInt(n, 10))
}

// groupedInThrees puts a space every three digits, counting from the right.
//
// Written out rather than reached for in a library because the one in the
// standard library is about money: golang.org/x/text/message formats to a
// LOCALE, and a locale is exactly what this must not have - the window and the
// command line have to say the same thing on a Polish desktop and an American
// one, and docs/UX.md has the surfaces agreeing as a rule rather than a hope.
func groupedInThrees(digits string) string {
sign := ""
if strings.HasPrefix(digits, "-") {
sign, digits = "-", digits[1:]
}
if len(digits) <= 3 {
return sign + digits
}
lead := len(digits) % 3
if lead == 0 {
lead = 3
}
var out strings.Builder
out.Grow(len(digits) + (len(digits)-1)/3)
out.WriteString(digits[:lead])
for i := lead; i < len(digits); i += 3 {
out.WriteByte(' ')
out.WriteString(digits[i : i+3])
}
return sign + out.String()
}

// Percent divides before multiplying where it has to, so a very large run does
// not wrap on the way to a number between nought and a hundred.
//
Expand Down
51 changes: 28 additions & 23 deletions internal/guard/actionbar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,26 @@ import (
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)

// The buttons that run something sit in the middle of the form.
// The buttons that run something sit at the right hand end of the bar.
//
// The owner's decision of 2026-08-19, and it REVERSES the one of 2026-08-18
// that this guard used to hold. Both came from looking at the built window, and
// the earlier reasoning is kept rather than deleted because it was a choice and
// not a mistake - a reader who sees only the new rule cannot tell whether the
// old one was ever tried.
// The owner's decision of 2026-09-08, and it is the THIRD answer this one
// question has had. All three came from looking at the built window and none
// was a mistake, so all three are kept: at the left because that is where a
// horizontal box puts things when nothing pushes them and nobody had chosen it
// (measured 2026-08-18, the bar 820 px wide with the buttons at x=0 and x=73),
// then at the right as the end of the reading path, then in the middle, and now
// at the right again.
//
// What it said before: they stood at the left, the owner asked why, and the
// honest answer was that nobody had chosen it - measured from the stored tree
// that morning, the bar is 820 px wide and the two buttons were at x=0 and
// x=73, which is where a horizontal box puts things when nothing pushes them.
// It was then set to the right edge, as the end of the reading path.
// What settled it this time was the two column body arriving. The middle of a
// bar has no relationship to the column somebody has just finished typing in,
// and the right hand end is where every desktop this ships to puts the button
// it wants pressed last.
//
// Written as a guard rather than left to the stored picture because the two
// answer different questions. The picture says the screen has not changed. This
// says which arrangement was chosen, so somebody reading the failure is told
// what the rule is rather than being handed two images to compare.
func TestTheButtonsThatRunSomethingSitInTheMiddle(t *testing.T) {
func TestTheButtonsThatRunSomethingSitAtTheRightHandEnd(t *testing.T) {
_, content := screenOnACanvas(t)

run := buttonNamed(content, text.ButtonGenerate())
Expand All @@ -48,21 +49,25 @@ func TestTheButtonsThatRunSomethingSitInTheMiddle(t *testing.T) {
t.Fatalf("there is no %q button, so this guard read the wrong tree", text.ButtonPreview())
}

// The group is measured rather than one button, because being centred is a
// property of the pair: either one alone can sit near the middle while the
// two of them are plainly off to one side.
// The group is measured rather than one button, because being at an end is
// a property of the pair: either one alone can sit near the edge while the
// two of them are plainly somewhere else.
before := first.Position().X
after := row.Size().Width - (run.Position().X + run.Size().Width)

if before <= slack || after <= slack {
t.Errorf("the run buttons are hard against an edge: %.1f px before them and %.1f px "+
"after, in a bar %.1f px wide. They were moved to the middle on 2026-08-19.",
before, after, row.Size().Width)
if before <= slack {
t.Errorf("the run buttons are hard against the LEFT edge: %.1f px before them, in a "+
"bar %.1f px wide. Nothing should be there - that end belongs to the rail.",
before, row.Size().Width)
}
if diff := before - after; diff > slack || diff < -slack {
t.Errorf("the run buttons are not centred: %.1f px before them and %.1f px after, in a "+
"bar %.1f px wide. What to do: keep a spacer at BOTH ends of the group in "+
"runner.actions, because one spacer can only push the group to an end.",
// Far more room before them than after. "After" is not zero because the key
// name that presses Generate stands past it, which is the one thing allowed
// to be further right than the last button.
if before <= after {
t.Errorf("the run buttons are not at the right hand end: %.1f px before them and %.1f px "+
"after, in a bar %.1f px wide. What to do: keep ONE greedy spacer in front of the "+
"group in runner.actions - a spacer pushes, so one in front puts the group at the far "+
"end and a second one behind it would put it back in the middle.",
before, after, row.Size().Width)
}
}
Expand Down
Loading
Loading