From fe8cba69873a5fd12c172c1f91edfd8e907f121d Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sat, 7 Nov 2026 23:16:33 -0300 Subject: [PATCH 01/22] fork: brand's files in a fork come from the brand commit the image pins Orchis held a hand-made copy of brand's palette SCSS and Bibata a colour table written beside it, each checked only against a local brand checkout, so the theme and the cursor could carry another palette than the image's palette.json, Ghostty's and VS Code's. As the site in spin takes brand's files at a pinned ref: fork brand writes a fork's copies from Construct-Linux/brand at the commit recipes/construct-brand.yaml pins, with that commit in the fork's brand.ref, and fork check fails a brand.ref or a copy other than that commit's. Co-Authored-By: Claude Opus 5.5 --- Taskfile.yml | 7 +- cmd/fork/brand.go | 161 +++++++++++++++++++++++++++++++++++++++++ cmd/fork/brand_test.go | 102 ++++++++++++++++++++++++++ cmd/fork/check.go | 13 ++++ cmd/fork/main.go | 7 +- 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 cmd/fork/brand.go create mode 100644 cmd/fork/brand_test.go diff --git a/Taskfile.yml b/Taskfile.yml index c4584c1..16d9703 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -408,10 +408,15 @@ tasks: - go run ./cmd/fork sync {{.CLI_ARGS}} fork:check: - desc: "A fork's tree holds: no conflict left, every .js and .ts parses, the schemas compile, JSON and XML parse, metadata declares GNOME Shell 51 alone, the recipe's patches apply: task fork:check -- gnome-shell-extension-vitals" + desc: "A fork's tree holds: no conflict left, every .js and .ts parses, the schemas compile, JSON and XML parse, metadata declares GNOME Shell 51 alone, the recipe's patches apply, its copies of brand's files are the image's brand commit's: task fork:check -- gnome-shell-extension-vitals" cmds: - go run ./cmd/fork check {{.CLI_ARGS}} + fork:brand: + desc: "Write a fork's copies of brand's files - Orchis' palette SCSS, Bibata's palette JSON - from Construct-Linux/brand at the commit recipes/construct-brand.yaml pins, and its brand.ref: task fork:brand -- orchis-theme. fork:check fails a copy other than that commit's." + cmds: + - go run ./cmd/fork brand {{.CLI_ARGS}} + fork:pin: desc: "Move a recipe to its fork's head, which must carry upstream's: upstream's version when newer, a new epoch otherwise: task fork:pin -- gnome-shell-extension-vitals. The comment above git-checkout is then a person's to update." cmds: diff --git a/cmd/fork/brand.go b/cmd/fork/brand.go new file mode 100644 index 0000000..29dd4af --- /dev/null +++ b/cmd/fork/brand.go @@ -0,0 +1,161 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Construct-Linux/os/internal/forks" +) + +// brandCopy is a file a fork holds a copy of from Construct-Linux/brand rather than reading it at +// build: palette.py's outputs, which brand checks (contrast, perception) before it writes them. +type brandCopy struct { + from, to string + // comment is how a line comment starts in the copy, for its provenance header; "" for a + // format with none (JSON), whose provenance is brand.ref's alone. + comment string +} + +// brandCopies are the forks that copy brand's files, by repository. Every copy comes from the +// one brand commit recipes/construct-brand.yaml pins - the commit the image's palette.json, +// Ghostty's and VS Code's themes come from - so the theme, the cursor and the rest of the image +// cannot hold two palettes. The fork records that commit in brand.ref. +var brandCopies = map[string][]brandCopy{ + "Orchis-theme": {{from: "palette/construct.scss", to: "src/_sass/_construct-palette.scss", comment: "//"}}, + "Bibata_Cursor": {{from: "palette/construct.json", to: "construct-palette.json"}}, +} + +const brandRef = "brand.ref" + +// fetcher reads a file of a repository under Construct-Linux at a commit. +type fetcher func(ctx context.Context, repo, commit, path string) ([]byte, error) + +// rawGitHub reads it from raw.githubusercontent.com: brand is public, and a commit's file there +// is that commit's, whatever has been pushed since. +func rawGitHub(ctx context.Context, repo, commit, path string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + url := fmt.Sprintf("https://raw.githubusercontent.com/Construct-Linux/%s/%s/%s", repo, commit, path) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s: %s", url, resp.Status) + } + return io.ReadAll(resp.Body) +} + +// brandPin is the brand commit recipes/construct-brand.yaml builds. +func brandPin(recipes string) (string, error) { + pin, err := forks.Find(recipes, "construct-brand") + if err != nil { + return "", err + } + if pin.Name != "brand" { + return "", fmt.Errorf("%s checks out %s, not brand", pin.Recipe, pin.Name) + } + return pin.Commit, nil +} + +// copied is a copy's bytes as the fork holds them: brand's file under a header naming where it +// came from, where the format has comments. +func copied(c brandCopy, commit string, raw []byte) []byte { + if c.comment == "" { + return raw + } + header := fmt.Sprintf("%s Copied from Construct-Linux/brand at brand.ref's commit (%s): its %s.\n"+ + "%s Change the colours in brand's palette.toml, pin brand in os, then task fork:brand.\n", + c.comment, commit[:12], c.from, c.comment) + return append([]byte(header), raw...) +} + +// writeBrand writes the fork's copies from brand at commit, and brand.ref. +func writeBrand(ctx context.Context, fetch fetcher, dir, repo, commit string) error { + for _, c := range brandCopies[repo] { + raw, err := fetch(ctx, "brand", commit, c.from) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(dir, c.to), copied(c, commit, raw), 0o644); err != nil { + return err + } + } + return os.WriteFile(filepath.Join(dir, brandRef), []byte(commit+"\n"), 0o644) +} + +// checkBrand is what is wrong with the fork's copies: brand.ref other than the image's brand +// commit, or a copy other than brand's file at it. +func checkBrand(ctx context.Context, fetch fetcher, dir, repo, commit string) ([]string, error) { + cs := brandCopies[repo] + if len(cs) == 0 { + return nil, nil + } + var problems []string + ref, err := os.ReadFile(filepath.Join(dir, brandRef)) + switch { + case errors.Is(err, os.ErrNotExist): + return []string{brandRef + " missing: task fork:brand writes it with the copies"}, nil + case err != nil: + return nil, err + } + if got := strings.TrimSpace(string(ref)); got != commit { + problems = append(problems, fmt.Sprintf("%s is %.12s, the image's brand (recipes/construct-brand.yaml) is %.12s: task fork:brand", brandRef, got, commit)) + } + for _, c := range cs { + raw, err := fetch(ctx, "brand", commit, c.from) + if err != nil { + return nil, err + } + have, err := os.ReadFile(filepath.Join(dir, c.to)) + if err != nil { + return nil, err + } + if string(have) != string(copied(c, commit, raw)) { + problems = append(problems, fmt.Sprintf("%s is not brand's %s at %.12s: task fork:brand", c.to, c.from, commit)) + } + } + return problems, nil +} + +// brandCmd (fork brand ) writes the fork's copies of brand's files at the commit the +// image pins brand to. +func brandCmd(ctx context.Context, args []string, out io.Writer) error { + fl := flag.NewFlagSet("brand", flag.ContinueOnError) + var w where + w.flags(fl) + name, err := parse(fl, args) + if err != nil { + return err + } + pin, err := forks.Find(w.recipes, name) + if err != nil { + return err + } + if len(brandCopies[pin.Name]) == 0 { + return fmt.Errorf("%s holds no copy of brand's files", pin.Name) + } + commit, err := brandPin(w.recipes) + if err != nil { + return err + } + dir := filepath.Join(w.forks, pin.Name) + if err := writeBrand(ctx, rawGitHub, dir, pin.Name, commit); err != nil { + return err + } + fmt.Fprintf(out, "%s: brand's files at %.12s, and %s\n", dir, commit, brandRef) + return nil +} diff --git a/cmd/fork/brand_test.go b/cmd/fork/brand_test.go new file mode 100644 index 0000000..9bcb804 --- /dev/null +++ b/cmd/fork/brand_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" +) + +// brandAt is a fake GitHub: brand's files by commit. +func brandAt(files map[string]map[string]string) fetcher { + return func(_ context.Context, repo, commit, path string) ([]byte, error) { + if repo != "brand" { + return nil, os.ErrNotExist + } + b, ok := files[commit][path] + if !ok { + return nil, os.ErrNotExist + } + return []byte(b), nil + } +} + +const ( + pinned = "1111111111111111111111111111111111111111" + older = "2222222222222222222222222222222222222222" +) + +var brandFiles = brandAt(map[string]map[string]string{ + pinned: {"palette/construct.scss": "$construct-light: #00E5FF;\n", "palette/construct.json": `{"cursor-fill": "#0B1116"}`}, + older: {"palette/construct.scss": "$construct-light: #00FFFF;\n", "palette/construct.json": `{"cursor-fill": "#000000"}`}, +}) + +func fork(t *testing.T, repo string) string { + t.Helper() + dir := t.TempDir() + for _, c := range brandCopies[repo] { + if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, c.to)), 0o755); err != nil { + t.Fatal(err) + } + } + return dir +} + +// What fork brand writes is what fork check takes, for each fork that copies brand. +func TestWrittenCopiesCheck(t *testing.T) { + ctx := context.Background() + for repo := range brandCopies { + dir := fork(t, repo) + if err := writeBrand(ctx, brandFiles, dir, repo, pinned); err != nil { + t.Fatal(err) + } + problems, err := checkBrand(ctx, brandFiles, dir, repo, pinned) + if err != nil || len(problems) > 0 { + t.Errorf("%s: %v %v", repo, problems, err) + } + } +} + +// Copies of another brand commit than the image's fail, and say so by brand.ref. +func TestCopiesOfAnotherBrandFail(t *testing.T) { + ctx := context.Background() + dir := fork(t, "Orchis-theme") + if err := writeBrand(ctx, brandFiles, dir, "Orchis-theme", older); err != nil { + t.Fatal(err) + } + problems, err := checkBrand(ctx, brandFiles, dir, "Orchis-theme", pinned) + if err != nil { + t.Fatal(err) + } + if len(problems) != 2 || !strings.Contains(problems[0], "brand.ref") { + t.Fatalf("problems = %q, want brand.ref's and the copy's", problems) + } +} + +// A copy edited by hand fails, though brand.ref is the image's. +func TestEditedCopyFails(t *testing.T) { + ctx := context.Background() + dir := fork(t, "Bibata_Cursor") + if err := writeBrand(ctx, brandFiles, dir, "Bibata_Cursor", pinned); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "construct-palette.json"), []byte(`{"cursor-fill": "#FF0000"}`), 0o644); err != nil { + t.Fatal(err) + } + problems, err := checkBrand(ctx, brandFiles, dir, "Bibata_Cursor", pinned) + if err != nil || len(problems) != 1 || !strings.Contains(problems[0], "construct-palette.json") { + t.Fatalf("problems = %q, err %v", problems, err) + } +} + +// The SCSS copy says where it came from; the JSON one is brand's bytes. +func TestCopiedHeader(t *testing.T) { + scss := string(copied(brandCopies["Orchis-theme"][0], pinned, []byte("x\n"))) + if !strings.HasPrefix(scss, "// Copied from Construct-Linux/brand") || !strings.HasSuffix(scss, "\nx\n") { + t.Errorf("scss copy = %q", scss) + } + if got := string(copied(brandCopies["Bibata_Cursor"][0], pinned, []byte("{}"))); got != "{}" { + t.Errorf("json copy = %q", got) + } +} diff --git a/cmd/fork/check.go b/cmd/fork/check.go index 5e8e278..016f94e 100644 --- a/cmd/fork/check.go +++ b/cmd/fork/check.go @@ -46,6 +46,19 @@ func checkCmd(ctx context.Context, args []string, out io.Writer) error { if marked, _ := r.lines(ctx, "grep", "-l", "-E", `^(<<<<<<< |>>>>>>> )`); len(marked) > 0 { fail("conflict markers left in: %s", strings.Join(marked, ", ")) } + if len(brandCopies[pin.Name]) > 0 { + commit, err := brandPin(w.recipes) + if err != nil { + return err + } + problems, err := checkBrand(ctx, rawGitHub, r.dir, pin.Name, commit) + if err != nil { + return err + } + for _, p := range problems { + fail("%s", p) + } + } files, err := r.lines(ctx, "ls-files") if err != nil { return err diff --git a/cmd/fork/main.go b/cmd/fork/main.go index 5cdc2cd..e212000 100644 --- a/cmd/fork/main.go +++ b/cmd/fork/main.go @@ -7,6 +7,8 @@ // XML parse, metadata declares GNOME Shell 51 alone, the recipe's patches apply (cmd/fork/check.go) // fork pin the recipe pins the fork's head, at upstream's version or a new epoch (cmd/fork/pin.go) // fork list every fork the recipes build from: recipe, repository, branch, commit, a line each +// fork brand the fork's copies of brand's files (the palette), at the brand commit the image pins +// (cmd/fork/brand.go); check fails a copy or a brand.ref other than that commit's // // A fork is upstream plus the fixes triaged against this stack, one commit each, on the branch // the recipe names; the checkout is <-forks>/, beside this repository by default, @@ -35,7 +37,8 @@ const usage = `usage: fork sync [-ours] [-recipes dir] [-forks dir] [-out dir] fork check [-recipes dir] [-forks dir] fork pin [-recipes dir] [-forks dir] [-out dir] - fork list [-recipes dir]` + fork list [-recipes dir] + fork brand [-recipes dir] [-forks dir]` // where is what every command is told: the recipes, the checkouts and the reports. type where struct { @@ -65,6 +68,8 @@ func main() { err = pinCmd(ctx, os.Args[2:], os.Stdout) case "list": err = listCmd(os.Args[2:], os.Stdout) + case "brand": + err = brandCmd(ctx, os.Args[2:], os.Stdout) default: err = fmt.Errorf("unknown command %q\n%s", os.Args[1], usage) } From e18dfa8ea75dbc1f966831fb868d861ca3fe9161 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sat, 7 Nov 2026 23:16:33 -0300 Subject: [PATCH 02/22] recipes: the design review's first round, and palettes from one brand brand on its ui-hill-climb branch: a quick toggle's on tint, chart series as System Monitor's dconf defaults (installed into local.d), and the light wallpapers' room in the accent. Orchis takes secondary text for dim-label, opaque notification cards, the on tint, today's disc and the palette from that brand commit; Bibata its colours from it. Each at a new epoch. Co-Authored-By: Claude Opus 5.5 --- recipes/bibata-cursor-theme.yaml | 7 ++++--- recipes/construct-brand.yaml | 11 +++++++---- recipes/orchis-theme.yaml | 7 ++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/recipes/bibata-cursor-theme.yaml b/recipes/bibata-cursor-theme.yaml index 072ef35..c11e0df 100644 --- a/recipes/bibata-cursor-theme.yaml +++ b/recipes/bibata-cursor-theme.yaml @@ -1,7 +1,7 @@ package: name: bibata-cursor-theme version: 2.0.7 - epoch: 5 + epoch: 6 description: Bibata Modern cursors in Construct's colours (Bibata-Modern-Construct) copyright: - license: GPL-3.0-or-later @@ -13,7 +13,8 @@ environment: - python-3.13 - rsvg-convert -# The fork's gnome-51 branch builds the theme from Bibata's SVGs with one Python program +# The fork's gnome-51 branch (its colours from the pinned brand's palette.json, brand.ref) builds +# the theme from Bibata's SVGs with one Python program # (build.py): the X cursors every client reads, and the cursors_scalable/ SVGs GNOME Shell 51 # draws its own cursor from - without them the shell logs "Could not load cursor" and shows # Adwaita's. @@ -22,7 +23,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Bibata_Cursor branch: gnome-51 - expected-commit: e602d9a4e06d4745fc8eed315289ddf4708bdcbd + expected-commit: d05f5aa4cf74dd32fd45ba8d72d66d0aa80831b2 - runs: | python3 build.py -o ${{targets.destdir}}/usr/share/icons diff --git a/recipes/construct-brand.yaml b/recipes/construct-brand.yaml index 07bb43c..bd7234c 100644 --- a/recipes/construct-brand.yaml +++ b/recipes/construct-brand.yaml @@ -5,12 +5,13 @@ # Background settings list it. The palette comes as data and as what apps outside GTK read: # palette.json, flat, for cmd/image (Plymouth's prompt, the harness's checks); Ghostty's Construct # Dark and Light themes, which `construct theme` seeds a config to; and VS Code's construct-theme, -# a built-in extension, so it is there before any account opens the editor. The images and the +# a built-in extension, so it is there before any account opens the editor; System Monitor's chart +# colours as dconf defaults (the palette's series). The images and the # palette's outputs are the ones brand commits and holds to its sources (its task check). package: name: construct-brand version: 0_git20261005 - epoch: 1 + epoch: 2 description: CONSTRUCT's logo, boot animation, wallpaper and palette copyright: - license: CC-BY-SA-4.0 @@ -26,8 +27,8 @@ pipeline: - uses: git-checkout with: repository: https://github.com/Construct-Linux/brand - branch: main - expected-commit: 32b719201ebc44bf6971c0b875d920cec041bd80 + branch: ui-hill-climb + expected-commit: 36d006e3ceb9bc45432fd546175f36615e710803 - runs: | d=${{targets.destdir}}/usr/share @@ -39,6 +40,8 @@ pipeline: cp logos/construct-symbolic.svg $d/icons/hicolor/symbolic/apps/construct-logo-symbolic.svg cp logos/io.github.construct_linux.Installer.svg logos/io.github.construct_linux.Construct.svg $d/icons/hicolor/scalable/apps/ cp palette/construct.json $d/construct/palette.json + # bake's dconf update compiles it into the local database with etc/dconf's own. + install -Dm644 palette/system-monitor.dconf ${{targets.destdir}}/etc/dconf/db/local.d/20-construct-system-monitor cp "palette/ghostty/Construct Dark" "palette/ghostty/Construct Light" $d/ghostty/themes/ # Its themes include VS Code's own 2026 ones for syntax by a path relative to the extensions # directory (theme-defaults), so it works only from there. diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index 2a7af2f..158abf7 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 17 + epoch: 18 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -22,12 +22,13 @@ pipeline: # The gnome-shell-51 branch of a fork, on upstream's master: Shell 51 only, and what 49-51 # changed in the shell's widgets that upstream's newest stylesheet (48) leaves unstyled; the # brand's palette as its one accent, and only GNOME Shell (dark: the shell is), GTK 3 and GTK 4. - # awk, which the build runs, is busybox's. + # The palette is copied from the pinned brand (brand.ref, task fork:brand). awk, which the build + # runs, is busybox's. - uses: git-checkout with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 13a4e67ca68e2dc68989285cf076fe0cd9e23e0e + expected-commit: 6358199a018c6a93d7b94ebfed92221c5ff9bec4 # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From ab001c0ae18780665cdd2f625ea29e8aca035980 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sat, 7 Nov 2026 23:43:34 -0300 Subject: [PATCH 03/22] image: the locks at the design review's first round construct-brand r2, orchis-theme r18 and bibata-cursor-theme r6, and nothing of Wolfi's. Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index 299feb7..759d048 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -17,7 +17,7 @@ contents: - at-spi2-core=2.62.1-r1 - at-spi2-core-lang=2.62.1-r1 - baobab=50.0-r0 - - bibata-cursor-theme=2.0.7-r5 + - bibata-cursor-theme=2.0.7-r6 - bluez=5.88-r1 - bluez-doc=5.88-r1 - bolt=0.9.11-r1 @@ -25,7 +25,7 @@ contents: - code=1.145.2-r0 - colord=1.4.8-r0 - colord-gtk=0.3.1-r0 - - construct-brand=0_git20261005-r1 + - construct-brand=0_git20261005-r2 - cracklib=2.10.4-r0 - cracklib-lang=2.10.4-r0 - cups-browsed=2.1.1-r1 @@ -245,8 +245,8 @@ contents: - openssh-sk-helper=10.6_p1-r1 - orca=51.1-r0 - orca-lang=51.1-r0 - - orchis-theme=20260914-r17 - - orchis-theme-shell=20260914-r17 + - orchis-theme=20260914-r18 + - orchis-theme-shell=20260914-r18 - ostree=2026.6-r0 - pam-u2f=1.4.0-r0 - pangomm=2.58.0-r0 From e2164c6e439e3558a64b1d761f653f0602eb8d47 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 00:15:47 -0300 Subject: [PATCH 04/22] image: -only leaves the session's checks out, and theme-dev takes apps A run wanting a few views - a design review's capture - still ran the session's checks and its idle measurement, about a third of the harness. They are scenarios now, system and idle, with installer beside them: -only runs what it names, the full run all of them. theme-dev took the shell's views only. With -only it also takes GTK's views and the apps and Settings panels named, as test does (the apps and panels in one method both share): the apps read the theme construct theme copied, so an Orchis change is judged on them in a minute or two on dev-up's machine instead of half an hour through the image and the stick, which stays the source of truth. Co-Authored-By: Claude Opus 5.5 --- Taskfile.yml | 2 +- cmd/image/dev.go | 18 ++++++ cmd/image/test.go | 135 +++++++++++++++++++++++------------------ cmd/image/test_test.go | 4 ++ 4 files changed, 100 insertions(+), 59 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 16d9703..6a21375 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -349,7 +349,7 @@ tasks: - go run ./cmd/image dev-down theme:dev: - desc: "Build the local Orchis checkout (ORCHIS, in .env) into the machine task dev:up runs - GNOME Shell loads it, construct theme copies GTK's - and take the shell's views against the needles: _output/test/look.html (task theme:dev -- -type adds the type matrix). About a minute; the image is still the source of truth (pin, rootfs, stick)." + desc: "Build the local Orchis checkout (ORCHIS, in .env) into the machine task dev:up runs - GNOME Shell loads it, construct theme copies GTK's - and take the shell's views against the needles: _output/test/look.html (task theme:dev -- -type adds the type matrix; -only views,panel:network,org.gnome.Nautilus.desktop adds GTK's views and those apps and panels, as test -only names them). About a minute; the image is still the source of truth (pin, rootfs, stick)." cmds: - go run ./cmd/image theme-dev -orchis "{{.ORCHIS}}" -needles "{{.NEEDLES}}" {{.CLI_ARGS}} diff --git a/cmd/image/dev.go b/cmd/image/dev.go index ed4cff3..0ca3b50 100644 --- a/cmd/image/dev.go +++ b/cmd/image/dev.go @@ -14,6 +14,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "strings" "syscall" "time" @@ -133,7 +134,12 @@ func themeDev(args []string) error { orchis := fl.String("orchis", "", "the Orchis-theme checkout to build (ORCHIS)") needles := fl.String("needles", "", "the needles checkout to compare with (NEEDLES)") withType := fl.Bool("type", false, "take the type specimen and its rendering matrix too (_output/test/type.html)") + only := fl.String("only", "", "apps and Settings panels to take too, as test -only names them, and views for GTK's own; the shell's views are always taken") _ = fl.Parse(args) + want, unknown := parseOnly(*only) + if len(unknown) > 0 { + return fmt.Errorf("theme-dev: -only names no app, panel: or views: %s", strings.Join(unknown, ", ")) + } if *orchis == "" { return errors.New("theme-dev: -orchis names an Orchis-theme checkout (ORCHIS)") } @@ -200,6 +206,18 @@ func themeDev(args []string) error { r.add("views' clock", false, "at "+viewsTime+": "+err.Error()) } r.shellViews(v, g, l) + // The apps take the theme as a person's do, from what construct theme copied: a design + // review's screens in a minute or two, where the image and the stick take half an hour. + if len(want) > 0 { + if err := g.holdReadings(); err != nil { + r.add("views' readings", false, err.Error()) + } + if want["views"] { + r.gtkViews(v, g, l, false) + r.gtkViews(v, g, l, true) + } + r.appsAndPanels(g, l, want) + } if *withType { r.typeMatrix(v, g, l) } diff --git a/cmd/image/test.go b/cmd/image/test.go index c98dab5..3063086 100644 --- a/cmd/image/test.go +++ b/cmd/image/test.go @@ -66,7 +66,7 @@ var panels = []string{"wifi", "network", "bluetooth", "display", "sound", "power func test(args []string) (err error) { fl := flag.NewFlagSet("test", flag.ExitOnError) - only := fl.String("only", "", "comma-separated scenario names (app ids, panel:, views, type, bench, suspend); all by default") + only := fl.String("only", "", "comma-separated scenario names (app ids, panel:, views, installer, system, idle, type, input, bench, suspend); all by default") stick := fl.Bool("usb", false, "boot the USB disk (task usb -- -test) as a machine does, instead of the ext4 test disk") needles := fl.String("needles", "", "a checkout of github.com/Construct-Linux/needles: every screenshot is compared with its needle") _ = fl.Parse(args) @@ -138,33 +138,44 @@ func test(args []string) (err error) { _, _ = g.run(asLive+`gsettings set org.gnome.desktop.privacy remember-app-usage false`, "") _, _ = g.eval(`Main.overview.hide(); true`) time.Sleep(time.Second) - r.installer(v, g, l, *stick) - // Before the desktop is shot: the stick's icon is gone again by then. - r.keyd(v, g) - r.usbStick(v, g) - r.appFolder(g) - r.screenReader(g) - // The live user's login keyring has no password (construct live): a secret is kept and read back - // with no prompt. A plaintext keyring reads as locked until something uses it, so the check - // does what an app does; a prompt would hang it until the timeout. - keyring, _ := g.run(keyringRoundTrip("live", 1000), "") - r.add("the login keyring is open", strings.TrimSpace(keyring) == "kept", oneLine(keyring)) - if *stick { - r.secureBoot(g) - r.dbxCurrent(g) - r.efiPrograms(g) - r.loaderCache(g) + // -only runs the scenarios it names and nothing else that can be left out: the session's own + // checks (system) and its measurement at rest (idle) took a third of a run that wanted a few + // views (a design review's capture). The desktop's view and the boot's are always taken. + want, unknown := parseOnly(*only) + for _, n := range unknown { + r.add("-only "+n, false, "no app, panel:, views, installer, system, idle, type, input, bench or suspend is called that") + } + runs := func(name string) bool { return len(want) == 0 || want[name] } + if runs("installer") { + r.installer(v, g, l, *stick) + } + if runs("system") { + // Before the desktop is shot: the stick's icon is gone again by then. + r.keyd(v, g) + r.usbStick(v, g) + r.appFolder(g) + r.screenReader(g) + // The live user's login keyring has no password (construct live): a secret is kept and read + // back with no prompt. A plaintext keyring reads as locked until something uses it, so the + // check does what an app does; a prompt would hang it until the timeout. + keyring, _ := g.run(keyringRoundTrip("live", 1000), "") + r.add("the login keyring is open", strings.TrimSpace(keyring) == "kept", oneLine(keyring)) + if *stick { + r.secureBoot(g) + r.dbxCurrent(g) + r.efiPrograms(g) + r.loaderCache(g) + } } l.shot(viewMeta{"desktop", needle.GroupDesktop, "The desktop at login over the default dark wallpaper: both bars opaque g with their ring, the top bar a 36 px pill 6 px from the edge, its text Adwaita Sans 400, and no accent", []string{"S1", "SP2", "R1", "T2", "E2"}}, (*lf).viewDesktop) - r.systemChecks(g) + if runs("system") { + r.systemChecks(g) + } // With the desktop up and nothing asked of it yet: what it does by itself. - r.idle(g) - - want, unknown := parseOnly(*only) - for _, n := range unknown { - r.add("-only "+n, false, "no app, panel:, views, type, input, bench or suspend is called that") + if runs("idle") { + r.idle(g) } // Every view from here is shot at viewsTime and with the bar's readings held: Calendar's // month is the views' month, and the date menu hangs under a clock that stays where it was. @@ -174,6 +185,48 @@ func test(args []string) (err error) { if err := g.holdReadings(); err != nil { r.add("views' readings", false, err.Error()) } + r.appsAndPanels(g, l, want) + if len(want) == 0 || want["views"] { + r.gtkViews(v, g, l, false) + r.gtkViews(v, g, l, true) + r.shellViews(v, g, l) + r.unlockLeaves(v, g) + r.desktopViews(v, g, l, sc) + l.guestLook() + } + l.finish() + if len(want) == 0 || want["type"] { + r.typeMatrix(v, g, l) + } + // After the views: the layout it switches to would be the views' too. + if len(want) == 0 || want["input"] { + r.deadKeys(v, g) + } + // After every view: the recording's errors - PipeWire refusing the DMA-BUF formats a VM + // without a GPU cannot give (withoutCastFormats) - are in the journal Logs shows. + if runs("system") { + r.screencast(g) + if *stick { + r.etcCopies(g) + } + } + // What the run needed, before the bench asks for more than there is. + r.memory(g) + r.console(g) + r.diagnose(g) + if len(want) == 0 || want["bench"] { + r.bench(g) + } + if len(want) == 0 || want["suspend"] { + r.suspend(v, g) + } + r.compareNeedles(*needles) + return r.write(g) +} + +// appsAndPanels opens each app and Settings panel want names (every one when it names none), shoots +// it and checks its surfaces: test's, and theme-dev's on a machine dev-up left running. +func (r *report) appsAndPanels(g *guest, l *lf, want map[string]bool) { for _, a := range apps { if len(want) > 0 && !want[a.id] { continue @@ -207,40 +260,6 @@ func test(args []string) (err error) { []string{"S3", "TY4", "TY5"}}, nil) } g.quitApp(settingsApp) - if len(want) == 0 || want["views"] { - r.gtkViews(v, g, l, false) - r.gtkViews(v, g, l, true) - r.shellViews(v, g, l) - r.unlockLeaves(v, g) - r.desktopViews(v, g, l, sc) - l.guestLook() - } - l.finish() - if len(want) == 0 || want["type"] { - r.typeMatrix(v, g, l) - } - // After the views: the layout it switches to would be the views' too. - if len(want) == 0 || want["input"] { - r.deadKeys(v, g) - } - // After every view: the recording's errors - PipeWire refusing the DMA-BUF formats a VM - // without a GPU cannot give (withoutCastFormats) - are in the journal Logs shows. - r.screencast(g) - if *stick { - r.etcCopies(g) - } - // What the run needed, before the bench asks for more than there is. - r.memory(g) - r.console(g) - r.diagnose(g) - if len(want) == 0 || want["bench"] { - r.bench(g) - } - if len(want) == 0 || want["suspend"] { - r.suspend(v, g) - } - r.compareNeedles(*needles) - return r.write(g) } // parseOnly is the scenarios -only names, all when it names none, and those of its names nothing @@ -252,7 +271,7 @@ func parseOnly(only string) (want map[string]bool, unknown []string) { want[n] = true } } - known := map[string]bool{"views": true, "type": true, "input": true, "bench": true, "suspend": true} + known := map[string]bool{"views": true, "installer": true, "system": true, "idle": true, "type": true, "input": true, "bench": true, "suspend": true} for _, a := range apps { known[a.id] = true } diff --git a/cmd/image/test_test.go b/cmd/image/test_test.go index e860c8b..3d42083 100644 --- a/cmd/image/test_test.go +++ b/cmd/image/test_test.go @@ -30,4 +30,8 @@ func TestParseOnly(t *testing.T) { if want, unknown := parseOnly(""); len(want) != 0 || len(unknown) != 0 { t.Errorf("all: %v %v", want, unknown) } + // The scenarios a partial run leaves out unless it names them. + if _, unknown := parseOnly("installer,system,idle"); len(unknown) != 0 { + t.Errorf("installer, system and idle unknown: %v", unknown) + } } From 4aad1db249cde9f9f08f3fa11c60de563ea677fe Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 00:21:33 -0300 Subject: [PATCH 05/22] installer: no disk is a state with a way out With no disk to install on, the welcome page said so in one line and offered nothing (the design review's round 1, two of three reviewers: a dead end). It now says what is missing and what to do, and Look Again reads the disks anew once one is connected. Co-Authored-By: Claude Opus 5.5 --- image/rootfs/usr/bin/construct-installer | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/image/rootfs/usr/bin/construct-installer b/image/rootfs/usr/bin/construct-installer index d69af49..e637721 100755 --- a/image/rootfs/usr/bin/construct-installer +++ b/image/rootfs/usr/bin/construct-installer @@ -111,12 +111,22 @@ class Installer(Adw.ApplicationWindow): description=_("Construct is running from this stick. Install it on this computer to keep it, " "or close this window and keep trying it."), ) + # No disk is a state with a way out, not a dead end (the design review's round 1): what is + # missing, what to do, and the one action that helps - looking again once a disk is in. if self.disk is None: - status.set_description(_("This computer has no disk Construct can be installed on.")) + status.set_description(_("This computer has no disk to install Construct on, other than the " + "stick it is running from. Connect one and look again, or close this " + "window and keep trying Construct.")) + status.set_child(self.button(_("Look Again"), "suggested-action", self.look_again)) else: status.set_child(self.button(_("Install…"), "suggested-action", lambda *args: self.nav.push(self.disk_page()))) return self.page(_("Welcome"), status, "welcome") + def look_again(self, *args): + self.disks = self.read_disks() + self.disk = next((d for d in self.disks if d["default"]), self.disks[0] if self.disks else None) + self.nav.replace([self.welcome_page()]) + def disk_page(self): group = Adw.PreferencesGroup( title=_("Where to install"), From d677ca06aeab5e9cfa11a34eda5e9af7547533e7 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 00:21:33 -0300 Subject: [PATCH 06/22] recipes: the design review's second round brand with the installer's own tile; Orchis with dimmed as secondary text and today on the on tint; Orchis and Bibata with the palette from that brand commit. Each at a new epoch. Co-Authored-By: Claude Opus 5.5 --- recipes/bibata-cursor-theme.yaml | 4 ++-- recipes/construct-brand.yaml | 4 ++-- recipes/orchis-theme.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/bibata-cursor-theme.yaml b/recipes/bibata-cursor-theme.yaml index c11e0df..5fe5f1c 100644 --- a/recipes/bibata-cursor-theme.yaml +++ b/recipes/bibata-cursor-theme.yaml @@ -1,7 +1,7 @@ package: name: bibata-cursor-theme version: 2.0.7 - epoch: 6 + epoch: 7 description: Bibata Modern cursors in Construct's colours (Bibata-Modern-Construct) copyright: - license: GPL-3.0-or-later @@ -23,7 +23,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Bibata_Cursor branch: gnome-51 - expected-commit: d05f5aa4cf74dd32fd45ba8d72d66d0aa80831b2 + expected-commit: bc8f408a34e29fe09ac8c0fa7d54b0c9c1882370 - runs: | python3 build.py -o ${{targets.destdir}}/usr/share/icons diff --git a/recipes/construct-brand.yaml b/recipes/construct-brand.yaml index bd7234c..4681b7f 100644 --- a/recipes/construct-brand.yaml +++ b/recipes/construct-brand.yaml @@ -11,7 +11,7 @@ package: name: construct-brand version: 0_git20261005 - epoch: 2 + epoch: 3 description: CONSTRUCT's logo, boot animation, wallpaper and palette copyright: - license: CC-BY-SA-4.0 @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/brand branch: ui-hill-climb - expected-commit: 36d006e3ceb9bc45432fd546175f36615e710803 + expected-commit: 79344fe23efa8b4a35530532c65a482086679908 - runs: | d=${{targets.destdir}}/usr/share diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index 158abf7..77010b2 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 18 + epoch: 19 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 6358199a018c6a93d7b94ebfed92221c5ff9bec4 + expected-commit: 437f40d8e0970e0b5b1372021f25da0ca9d620f2 # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From 04ec8da212f21e109980c41afff8df84313cc884 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 00:36:33 -0300 Subject: [PATCH 07/22] orchis-theme: Files' selected place at the pane's edge Co-Authored-By: Claude Opus 5.5 --- recipes/orchis-theme.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index 77010b2..682b34a 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 19 + epoch: 20 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 437f40d8e0970e0b5b1372021f25da0ca9d620f2 + expected-commit: 2f2c0496e198a41c253780459a1492e76f31effb # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From 334b26d36d855aedfea40fd2f825377af1c30c4d Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 00:48:46 -0300 Subject: [PATCH 08/22] image: the locks at the design review's second round Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index 759d048..e9532c4 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -17,7 +17,7 @@ contents: - at-spi2-core=2.62.1-r1 - at-spi2-core-lang=2.62.1-r1 - baobab=50.0-r0 - - bibata-cursor-theme=2.0.7-r6 + - bibata-cursor-theme=2.0.7-r7 - bluez=5.88-r1 - bluez-doc=5.88-r1 - bolt=0.9.11-r1 @@ -25,7 +25,7 @@ contents: - code=1.145.2-r0 - colord=1.4.8-r0 - colord-gtk=0.3.1-r0 - - construct-brand=0_git20261005-r2 + - construct-brand=0_git20261005-r3 - cracklib=2.10.4-r0 - cracklib-lang=2.10.4-r0 - cups-browsed=2.1.1-r1 @@ -245,8 +245,8 @@ contents: - openssh-sk-helper=10.6_p1-r1 - orca=51.1-r0 - orca-lang=51.1-r0 - - orchis-theme=20260914-r18 - - orchis-theme-shell=20260914-r18 + - orchis-theme=20260914-r20 + - orchis-theme-shell=20260914-r20 - ostree=2026.6-r0 - pam-u2f=1.4.0-r0 - pangomm=2.58.0-r0 From 940c79afdfa84f34022491a05482ea706a12e1c1 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 01:39:27 -0300 Subject: [PATCH 09/22] construct-app: a page's title is said once Keys, Updates and Tailscale named their first group after the page, 50px under the header bar's same title (round 2's council): the group keeps its description. Co-Authored-By: Claude Opus 5.5 --- image/rootfs/usr/bin/construct-app | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/image/rootfs/usr/bin/construct-app b/image/rootfs/usr/bin/construct-app index 7cc615b..8495c89 100755 --- a/image/rootfs/usr/bin/construct-app +++ b/image/rootfs/usr/bin/construct-app @@ -80,7 +80,6 @@ class KeysPage(Adw.PreferencesPage): self.window = window self.encrypted = False self.group = Adw.PreferencesGroup( - title=_("Security keys"), description=_("A key you add opens this computer's disk with its PIN and a touch, and signs you in " "where Construct asks for your password - sudo, the lock screen, a change to the " "system - with a touch. Your password keeps working without it. Add a second key " @@ -229,8 +228,7 @@ class UpdatesPage(Adw.PreferencesPage): super().__init__(title=_("Updates"), icon_name="software-update-available-symbolic") self.window = window rel = os_release() - group = Adw.PreferencesGroup(title=_("Updates"), - description=_("Construct updates itself once a day and starts the new release at " + group = Adw.PreferencesGroup(description=_("Construct updates itself once a day and starts the new release at " "the next restart. If it does not come up, the one before starts again.")) version = Adw.ActionRow(title=_("Running"), subtitle=rel.get("IMAGE_VERSION", _("a development build"))) group.add(version) @@ -368,8 +366,7 @@ class TailscalePage(Adw.PreferencesPage): def __init__(self, window): super().__init__(title=_("Tailscale"), icon_name="network-vpn-symbolic") self.window = window - group = Adw.PreferencesGroup(title=_("Tailscale"), - description=_("A private network between your own devices. Nothing reaches this " + group = Adw.PreferencesGroup(description=_("A private network between your own devices. Nothing reaches this " "computer through it that you did not open.")) self.state = Adw.ActionRow(title=_("State")) self.button = Gtk.Button(valign=Gtk.Align.CENTER) From 94d4870d4beb9fc10c245758557970c01d8eaaa4 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 01:39:27 -0300 Subject: [PATCH 10/22] recipes: the design review's third round brand at 0989b1f (chart series four hues apart); Orchis at 5652657 (an off switch's knob, a quiet empty notification list); Bibata at 6aa8079 (the palette copy). Co-Authored-By: Claude Opus 5.5 --- recipes/bibata-cursor-theme.yaml | 4 ++-- recipes/construct-brand.yaml | 4 ++-- recipes/orchis-theme.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/bibata-cursor-theme.yaml b/recipes/bibata-cursor-theme.yaml index 5fe5f1c..4ca6de7 100644 --- a/recipes/bibata-cursor-theme.yaml +++ b/recipes/bibata-cursor-theme.yaml @@ -1,7 +1,7 @@ package: name: bibata-cursor-theme version: 2.0.7 - epoch: 7 + epoch: 8 description: Bibata Modern cursors in Construct's colours (Bibata-Modern-Construct) copyright: - license: GPL-3.0-or-later @@ -23,7 +23,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Bibata_Cursor branch: gnome-51 - expected-commit: bc8f408a34e29fe09ac8c0fa7d54b0c9c1882370 + expected-commit: 6aa8079aaf2d9a8822e3519789f5562a35c21a1a - runs: | python3 build.py -o ${{targets.destdir}}/usr/share/icons diff --git a/recipes/construct-brand.yaml b/recipes/construct-brand.yaml index 4681b7f..71eac18 100644 --- a/recipes/construct-brand.yaml +++ b/recipes/construct-brand.yaml @@ -11,7 +11,7 @@ package: name: construct-brand version: 0_git20261005 - epoch: 3 + epoch: 4 description: CONSTRUCT's logo, boot animation, wallpaper and palette copyright: - license: CC-BY-SA-4.0 @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/brand branch: ui-hill-climb - expected-commit: 79344fe23efa8b4a35530532c65a482086679908 + expected-commit: 0989b1f4ef2151355f613ea369caf96391a3a846 - runs: | d=${{targets.destdir}}/usr/share diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index 682b34a..71ac53b 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 20 + epoch: 21 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 2f2c0496e198a41c253780459a1492e76f31effb + expected-commit: 56526572d8ca5b08b97bae5ccc6320454d37fba1 # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From 8ed5123160a5304e5bf2cc907b784a044ffa5ca2 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 01:55:43 -0300 Subject: [PATCH 11/22] image: the locks at the design review's third round Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index e9532c4..aa58795 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -17,7 +17,7 @@ contents: - at-spi2-core=2.62.1-r1 - at-spi2-core-lang=2.62.1-r1 - baobab=50.0-r0 - - bibata-cursor-theme=2.0.7-r7 + - bibata-cursor-theme=2.0.7-r8 - bluez=5.88-r1 - bluez-doc=5.88-r1 - bolt=0.9.11-r1 @@ -25,7 +25,7 @@ contents: - code=1.145.2-r0 - colord=1.4.8-r0 - colord-gtk=0.3.1-r0 - - construct-brand=0_git20261005-r3 + - construct-brand=0_git20261005-r4 - cracklib=2.10.4-r0 - cracklib-lang=2.10.4-r0 - cups-browsed=2.1.1-r1 @@ -245,8 +245,8 @@ contents: - openssh-sk-helper=10.6_p1-r1 - orca=51.1-r0 - orca-lang=51.1-r0 - - orchis-theme=20260914-r20 - - orchis-theme-shell=20260914-r20 + - orchis-theme=20260914-r21 + - orchis-theme-shell=20260914-r21 - ostree=2026.6-r0 - pam-u2f=1.4.0-r0 - pangomm=2.58.0-r0 From 7108214b069c774033aec411dce7e67c74ef7158 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 02:34:45 -0300 Subject: [PATCH 12/22] recipes: orchis-theme for the design review's fourth round An insensitive Clear drawn transparent (St reads no opacity), and a dialog dimming its dark parent by half. Co-Authored-By: Claude Opus 5.5 --- recipes/orchis-theme.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index 71ac53b..bc9f50b 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 21 + epoch: 22 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 56526572d8ca5b08b97bae5ccc6320454d37fba1 + expected-commit: 41fd4d480067b3eb0332e4aae5cea0452e0c564f # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From b95b4aabf7f9836daf31fbe5783a0f080b4744f3 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 02:52:16 -0300 Subject: [PATCH 13/22] image: the locks at the design review's fourth round Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index aa58795..f00bedf 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -245,8 +245,8 @@ contents: - openssh-sk-helper=10.6_p1-r1 - orca=51.1-r0 - orca-lang=51.1-r0 - - orchis-theme=20260914-r21 - - orchis-theme-shell=20260914-r21 + - orchis-theme=20260914-r22 + - orchis-theme-shell=20260914-r22 - ostree=2026.6-r0 - pam-u2f=1.4.0-r0 - pangomm=2.58.0-r0 From 5c4c0378c2a7a881e1b5a97852d4feac4b7a2f47 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 11:23:13 -0300 Subject: [PATCH 14/22] image: lf checks on as the on tint with text THEMING's row 3 now draws a quick toggle that is on, and today selected, as the on tint with text as the label; the toggles, states and today checks read those tokens instead of the rest steps and accent_text. Co-Authored-By: Claude Opus 5.5 --- cmd/image/lf.go | 39 +++++++++++++++++++-------------------- cmd/image/lf_test.go | 6 +++--- cmd/image/test.go | 6 +++--- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/cmd/image/lf.go b/cmd/image/lf.go index 7bbf2f5..e35a1d4 100644 --- a/cmd/image/lf.go +++ b/cmd/image/lf.go @@ -41,10 +41,9 @@ const ( lfAccentTol = 14 // The calendar's today ring is 2 px on a curve: its pixels are half blends of the accent. lfRingAccentTol = 40 - // A quick toggle's title on its tint, and the focus ring's inset shadow over a toggle: thin - // accent strokes on a fill, so, like the today ring, most of their pixels are blends of it. - lfAccentTitleTol = 40 - lfFocusRingTol = 40 + // The focus ring's inset shadow over a toggle: thin accent strokes on a fill, so, like the + // today ring, most of their pixels are blends of it. + lfFocusRingTol = 40 // The ring against what is 6 px outside it: the brand's minimum edge contrast. lfEdgeContrast = 1.5 lfEdgeGap = 6 @@ -133,7 +132,7 @@ var lfTokens = paletteTokens{ colors: []string{"brand-boot", "neutral-a", "neutral-e", "neutral-f", "neutral-g", "neutral-h", "dark-border", "dark-accent", "dark-accent-text", "dark-text", "dark-muted", "dark-on-accent", "light-accent", "light-accent-text", "light-on-accent", "light-text", "light-muted", "light-desk", "light-shelf"}, - nums: []string{"tint-dark-rest", "tint-dark-hover", "tint-dark-active", "line-light-ring", "line-light-border", "shape-inset", "shape-radius-control", + nums: []string{"tint-dark-rest", "tint-dark-hover", "tint-dark-active", "tint-dark-on", "tint-dark-on-hover", "line-light-ring", "line-light-border", "shape-inset", "shape-radius-control", "shape-radius-surface", "shape-radius-sheet"}, strs: []string{"type-ui", "type-mono"}, } @@ -849,7 +848,7 @@ func (l *lf) quickSettings(img image.Image, s quickSettingsSeen) { n := pixels.CountNear(img, pixels.Rect{X: 0, Y: 0, W: img.Bounds().Dx(), H: img.Bounds().Dy()}, l.c("dark-accent"), lfAccentTol) most := lfAccentQuickSettings * l.k * l.k l.check("accent", "quick settings", n < most, fmt.Sprintf("%d px of the accent, want under %d", n, most)) - fill := pixels.Tint(l.c("dark-accent"), h, l.pal.nums["tint-dark-rest"]) + fill := pixels.Tint(l.c("dark-accent"), h, l.pal.nums["tint-dark-on"]) ground := l.c("neutral-e") var on, off int var bad, discs []string @@ -864,10 +863,10 @@ func (l *lf) quickSettings(img image.Image, s quickSettingsSeen) { on++ mode, _ := pixels.ModeColor(img, l.px(t.Look.rect()).Inset(4*l.k, 4*l.k)) if !pixels.Near(mode, fill, lfTextTol) { - bad = append(bad, fmt.Sprintf("%q on is %s, not the tint %s", t.Title.Text, pixels.Hex(mode), pixels.Hex(fill))) + bad = append(bad, fmt.Sprintf("%q on is %s, not the on tint %s", t.Title.Text, pixels.Hex(mode), pixels.Hex(fill))) } - if c := t.Title.ink(ground); !pixels.Near(c, l.c("dark-accent-text"), lfTextTol) || pixels.CountNear(img, l.px(t.Title.rect()), l.c("dark-accent-text"), lfAccentTitleTol) == 0 { - bad = append(bad, fmt.Sprintf("%q on titled %s, not the accent", t.Title.Text, pixels.Hex(c))) + if c := t.Title.ink(ground); !pixels.Near(c, l.c("dark-text"), lfTextTol) { + bad = append(bad, fmt.Sprintf("%q on titled %s, not text", t.Title.Text, pixels.Hex(c))) } if t.Subtitle != nil && !pixels.Near(t.Subtitle.ink(ground), l.c("dark-text"), lfTextTol) { bad = append(bad, fmt.Sprintf("%q on subtitled %s, not text: muted never goes on a tint", t.Title.Text, pixels.Hex(t.Subtitle.ink(ground)))) @@ -1053,7 +1052,8 @@ func (l *lf) calendar(img image.Image, s calendarSeen) { } // today is F1's today: accent_text at 700 with no ring and no box-shadow, unless it has the key -// focus; the menu opens on it, so it is the selected day too, the rest tint inside. +// focus; the menu opens on it, so it is the selected day too, and selected today is on: the on +// tint inside, text at 700. func (l *lf) today(img image.Image, days []calendarDay, selected, focus bool) { var t *lfLook for _, d := range days { @@ -1066,22 +1066,21 @@ func (l *lf) today(img image.Image, days []calendarDay, selected, focus bool) { return } h := l.c("neutral-h") - centre := h + centre, ink := h, l.c("dark-accent-text") if selected { - centre = pixels.Tint(l.c("dark-accent"), h, l.pal.nums["tint-dark-rest"]) + centre, ink = pixels.Tint(l.c("dark-accent"), h, l.pal.nums["tint-dark-on"]), l.c("dark-text") } mode, _ := pixels.ModeColor(img, l.px(t.rect()).Inset(4*l.k, 4*l.k)) share := pixels.RingShare(img, l.px(t.rect()), float64(l.k), l.c("dark-accent"), lfRingAccentTol) fg, _ := pixels.ParseHex(t.FG) - accent := l.c("dark-accent-text") shadow := t.Shadow != nil && !strings.HasSuffix(t.Shadow.Color, "00") - ok := pixels.Near(mode, centre, lfTextTol) && t.Weight == 700 && fg == color.NRGBA{accent.R, accent.G, accent.B, 255} + ok := pixels.Near(mode, centre, lfTextTol) && t.Weight == 700 && fg == color.NRGBA{ink.R, ink.G, ink.B, 255} if !focus { ok = ok && !shadow && share < lfTodayNoRing } l.check("today", "calendar", ok, fmt.Sprintf("centre %s, want %s (selected %v); %s %d, want %s 700; box-shadow %v, %.0f%% of its outline the accent, want none and under %.0f%% (focused %v)", - pixels.Hex(mode), pixels.Hex(centre), selected, t.FG, t.Weight, pixels.Hex(accent), shadow, share*100, lfTodayNoRing*100, focus)) + pixels.Hex(mode), pixels.Hex(centre), selected, t.FG, t.Weight, pixels.Hex(ink), shadow, share*100, lfTodayNoRing*100, focus)) } // focusRing is whether St's box-shadow is the key focus's ring - inset, 2 px or more, the accent @@ -1361,10 +1360,10 @@ const lfStates = lfQuickSettings + ` return lfAll(m.box, t => t._lfState !== undefined).map(t => ({state: t._lfState, look: look(t), title: look(t._title ?? null)}));` // states is S2 on the quick toggles shell-states forced into each step - on at rest, under the -// pointer, pressed: each fill is the accent at its step's tint over h, in St and on the screen, -// and each label accent_text. +// pointer, pressed: each fill is the accent at its step's tint over h, in St and on the screen - +// the on tint at rest, on-hover under the pointer and pressed - and each label text. func (l *lf) states(img image.Image, s []stateSeen) { - steps := map[string]string{"rest": "tint-dark-rest", "hover": "tint-dark-hover", "active": "tint-dark-active"} + steps := map[string]string{"rest": "tint-dark-on", "hover": "tint-dark-on-hover", "active": "tint-dark-on-hover"} h, ground, accent := l.c("neutral-h"), l.c("neutral-e"), l.c("dark-accent") seen := map[string]bool{} var bad []string @@ -1380,8 +1379,8 @@ func (l *lf) states(img image.Image, s []stateSeen) { if !pixels.Near(st, want, lfTextTol) || !pixels.Near(drawn, want, lfTextTol) { bad = append(bad, fmt.Sprintf("%s: St %s, drawn %s, want %s (%s)", t.State, pixels.Hex(st), pixels.Hex(drawn), pixels.Hex(want), tok)) } - if t.Title != nil && !pixels.Near(t.Title.ink(ground), l.c("dark-accent-text"), lfTextTol) { - bad = append(bad, fmt.Sprintf("%s: %q titled %s, not accent_text", t.State, t.Title.Text, pixels.Hex(t.Title.ink(ground)))) + if t.Title != nil && !pixels.Near(t.Title.ink(ground), l.c("dark-text"), lfTextTol) { + bad = append(bad, fmt.Sprintf("%s: %q titled %s, not text", t.State, t.Title.Text, pixels.Hex(t.Title.ink(ground)))) } } l.check("states", "quick toggles", len(seen) == len(steps) && len(bad) == 0, diff --git a/cmd/image/lf_test.go b/cmd/image/lf_test.go index b17dab1..f9268f2 100644 --- a/cmd/image/lf_test.go +++ b/cmd/image/lf_test.go @@ -60,7 +60,7 @@ const tPalette = `{"brand-boot":"#000000","neutral-e":"#0B1116","neutral-f":"#11 "dark-text":"#E6EDF2","dark-muted":"#9FB3C0","dark-on-accent":"#0B1116","light-accent":"#007A85","light-accent-text":"#00646D", "light-on-accent":"#FFFFFF","light-text":"#11181E","light-muted":"#536672","light-desk":"#DEE4E8","light-shelf":"#E9EDF0", "neutral-a":"#FFFFFF","line-light-ring":0.4,"line-light-border":0.3,"ansi-dark-blue":"#5DB2F7", -"tint-dark-rest":0.16,"tint-dark-hover":0.23,"tint-dark-active":0.28,"shape-inset":6,"shape-radius-control":9,"shape-radius-surface":15,"shape-radius-sheet":24, +"tint-dark-rest":0.16,"tint-dark-hover":0.23,"tint-dark-active":0.28,"tint-dark-on":0.36,"tint-dark-on-hover":0.4,"shape-inset":6,"shape-radius-control":9,"shape-radius-surface":15,"shape-radius-sheet":24, "shape-space":[3,6,12,18,24],"type-ui":"Adwaita Sans","type-mono":"Adwaita Mono"}` // palette.json is flat and mixed - colours, numbers, families, a list - and a token a check needs @@ -657,12 +657,12 @@ func TestStatesView(t *testing.T) { h, accent := l.c("neutral-h"), l.c("dark-accent") img := solid(640, 400, h) var s []stateSeen - for i, st := range []struct{ state, tok string }{{"rest", "tint-dark-rest"}, {"hover", "tint-dark-hover"}, {"active", "tint-dark-active"}} { + for i, st := range []struct{ state, tok string }{{"rest", "tint-dark-on"}, {"hover", "tint-dark-on-hover"}, {"active", "tint-dark-on-hover"}} { want := pixels.Tint(accent, h, l.pal.nums[st.tok]) r := pixels.Rect{X: 20 + 120*i, Y: 20, W: 100, H: 40} fill(img, r, want) look := &lfLook{X: r.X, Y: r.Y, W: r.W, H: r.H, Chain: []string{pixels.Hexa(want)}} - title := &lfLook{FG: pixels.Hexa(l.c("dark-accent-text")), Opacity: 1, Chain: []string{pixels.Hexa(want)}} + title := &lfLook{FG: pixels.Hexa(l.c("dark-text")), Opacity: 1, Chain: []string{pixels.Hexa(want)}} s = append(s, stateSeen{State: st.state, Look: look, Title: title}) } l.states(img, s) diff --git a/cmd/image/test.go b/cmd/image/test.go index 3063086..d636424 100644 --- a/cmd/image/test.go +++ b/cmd/image/test.go @@ -669,7 +669,7 @@ var shellViews = []shellView{ "A folder of the app grid open: its dialog on g with the ring and its radius, its name at the top, the apps it holds in a grid under it", []string{"S3", "E3"}, nil}, {"quick-settings", `Main.panel.statusArea.quickSettings.menu.open()`, `Main.panel.statusArea.quickSettings.menu.close()`, nil, 0, - "Quick settings: the menu on h with its ring and concentric radius, toggles on as the rest tint with the accent's 24 px disc and accent_text, off in text", + "Quick settings: the menu on h with its ring and concentric radius, toggles on as the on tint with the accent's 24 px disc and text, off in text", []string{"S2", "C3", "C4", "R1"}, (*lf).viewQuickSettings}, // The first quick toggle on screen with the key focus, once the menu is open: its focus ring, // against the view above. Hidden toggles (no Bluetooth, no Wi-Fi) come first in the tree. @@ -690,10 +690,10 @@ var shellViews = []shellView{ {"states", `const m = Main.panel.statusArea.quickSettings.menu; m.open(); GLib.timeout_add(GLib.PRIORITY_DEFAULT, 800, () => { lfAll(m.box, lfClass('quick-toggle')).filter(t => t.is_mapped() && t.reactive).slice(0, 3).forEach((t, i) => { t._lfAdded = [['checked'], ['checked', 'hover'], ['checked', 'active']][i].filter(c => !t.has_style_pseudo_class(c)); t._lfAdded.forEach(c => t.add_style_pseudo_class(c)); t._lfState = ['rest', 'hover', 'active'][i]; }); return GLib.SOURCE_REMOVE; })`, `const m = Main.panel.statusArea.quickSettings.menu; lfAll(m.box, t => t._lfState !== undefined).forEach(t => { t._lfAdded.forEach(c => t.remove_style_pseudo_class(c)); delete t._lfAdded; delete t._lfState; }); m.close()`, nil, 0, - "Quick toggles on at rest, under the pointer and pressed: each the accent's tint at its own step, the label accent_text on all three", + "Quick toggles on at rest, under the pointer and pressed: the on tint at rest, its hover step under the pointer and pressed, the label text on all three", []string{"S2", "C3"}, (*lf).viewStates}, {"calendar", `Main.panel.statusArea.dateMenu.menu.open()`, `Main.panel.statusArea.dateMenu.menu.close()`, nil, 0, - "The date menu: the calendar on h at the quick settings' height, today accent_text at 700 with no ring, the month's days in text, the other months' muted", + "The date menu: the calendar on h at the quick settings' height, today on (the on tint, text at 700) with no ring, the month's days in text, the other months' muted", []string{"S2", "F1", "C2", "R1"}, (*lf).viewCalendar}, // A day that is not today with the key focus: the ring is focus's alone (F1). {"calendar-focus", From af8060f11fd87190d0c62ecc51a4d1ed6397d763 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 11:28:20 -0300 Subject: [PATCH 15/22] image: tilingshell's border is the brand's cyan Without window-use-custom-border-color tilingshell draws St's accent, GNOME's teal: the cyan set beside it was never read. lf checks the key with the colour. Co-Authored-By: Claude Opus 5.5 --- cmd/image/lf.go | 4 ++-- image/rootfs/etc/dconf/db/local.d/00-construct | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/image/lf.go b/cmd/image/lf.go index e35a1d4..f8bd8f5 100644 --- a/cmd/image/lf.go +++ b/cmd/image/lf.go @@ -1850,10 +1850,10 @@ func (l *lf) guestFiles() { want := fmt.Sprintf("'%s 11'\n'%s 11'\n%s\n%s\n%s\n", ui, mono, mono, ui, ui) l.check("fonts", "G4", err == nil && fonts == want, fmt.Sprintf("font-name, monospace-font-name, fc-match monospace/sans-serif/system-ui: %s; want %s", oneLine(fonts), oneLine(want))+errText(err)) - dconf, err := l.g.run(`for k in dash-to-panel/focus-highlight-color dash-to-panel/focus-highlight-opacity dash-to-panel/trans-use-border tilingshell/window-border-color; do `+asLive+`dconf read /org/gnome/shell/extensions/$k; done`, "") + dconf, err := l.g.run(`for k in dash-to-panel/focus-highlight-color dash-to-panel/focus-highlight-opacity dash-to-panel/trans-use-border tilingshell/window-border-color tilingshell/window-use-custom-border-color; do `+asLive+`dconf read /org/gnome/shell/extensions/$k; done`, "") accent := pixels.Hex(l.c("dark-accent")) // The dock's focus highlight is the rest tint, in dash-to-panel's percent. - wantDconf := fmt.Sprintf("'%s'\n%.0f\nfalse\n'%s'\n", accent, l.pal.nums["tint-dark-rest"]*100, accent) + wantDconf := fmt.Sprintf("'%s'\n%.0f\nfalse\n'%s'\ntrue\n", accent, l.pal.nums["tint-dark-rest"]*100, accent) l.check("extensions", "G5 settings", err == nil && strings.EqualFold(dconf, wantDconf), fmt.Sprintf("%s; want %s", oneLine(dconf), oneLine(wantDconf))+errText(err)) for _, uuid := range []string{"dash-to-panel@jderose9.github.com", "tilingshell@ferrarodomenico.com", "Vitals@CoreCoding.com"} { sheet, err := l.g.run("cat /usr/share/gnome-shell/extensions/"+uuid+"/stylesheet.css", "") diff --git a/image/rootfs/etc/dconf/db/local.d/00-construct b/image/rootfs/etc/dconf/db/local.d/00-construct index 5745470..1f30632 100644 --- a/image/rootfs/etc/dconf/db/local.d/00-construct +++ b/image/rootfs/etc/dconf/db/local.d/00-construct @@ -113,9 +113,11 @@ dot-style-unfocused='DOTS' trans-use-border=false dot-color-override=false -# The focused window's border, when a person turns it on, is the accent. +# The focused window's border, when a person turns it on, is the accent: without the custom colour +# tilingshell draws St's accent, GNOME's teal, not the brand's cyan. [org/gnome/shell/extensions/tilingshell] window-border-color='#00E5FF' +window-use-custom-border-color=true [org/gnome/shell/extensions/just-perfection] clock-menu-position=1 From c4e1000be3298d7075b74b47a9765a782ea9a956 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 11:57:24 -0300 Subject: [PATCH 16/22] image: test Wi-Fi among a dozen networks, the lock screen with its hint The harness had no Wi-Fi: Settings' panel said no adapter, and the quick settings menu that ran off the screen with networks around was never seen. The wifi scenario puts twelve networks in the air with mac80211_hwsim and wpa_supplicant's AP mode, each a unit of its own, and checks that the menu with the Wi-Fi list open stays whole on the screen; Settings' panel is shot with the list. The lock screen was shot 2 s after the pointer woke it: the shell shows how to unlock only after 4 s without input, and the view a person looks at has the hint. Co-Authored-By: Claude Opus 5.5 --- cmd/image/test.go | 23 +++++++++--- cmd/image/wifi.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 cmd/image/wifi.go diff --git a/cmd/image/test.go b/cmd/image/test.go index d636424..1723fd5 100644 --- a/cmd/image/test.go +++ b/cmd/image/test.go @@ -66,7 +66,7 @@ var panels = []string{"wifi", "network", "bluetooth", "display", "sound", "power func test(args []string) (err error) { fl := flag.NewFlagSet("test", flag.ExitOnError) - only := fl.String("only", "", "comma-separated scenario names (app ids, panel:, views, installer, system, idle, type, input, bench, suspend); all by default") + only := fl.String("only", "", "comma-separated scenario names (app ids, panel:, views, installer, system, idle, type, input, bench, suspend, wifi); all by default") stick := fl.Bool("usb", false, "boot the USB disk (task usb -- -test) as a machine does, instead of the ext4 test disk") needles := fl.String("needles", "", "a checkout of github.com/Construct-Linux/needles: every screenshot is compared with its needle") _ = fl.Parse(args) @@ -143,7 +143,7 @@ func test(args []string) (err error) { // views (a design review's capture). The desktop's view and the boot's are always taken. want, unknown := parseOnly(*only) for _, n := range unknown { - r.add("-only "+n, false, "no app, panel:, views, installer, system, idle, type, input, bench or suspend is called that") + r.add("-only "+n, false, "no app, panel:, views, installer, system, idle, type, input, bench, suspend or wifi is called that") } runs := func(name string) bool { return len(want) == 0 || want[name] } if runs("installer") { @@ -194,6 +194,11 @@ func test(args []string) (err error) { r.desktopViews(v, g, l, sc) l.guestLook() } + // After the views: the radios it adds put a Wi-Fi toggle in quick settings, which their + // needles do not have. + if len(want) == 0 || want["wifi"] { + r.wifi(g, l) + } l.finish() if len(want) == 0 || want["type"] { r.typeMatrix(v, g, l) @@ -271,7 +276,7 @@ func parseOnly(only string) (want map[string]bool, unknown []string) { want[n] = true } } - known := map[string]bool{"views": true, "installer": true, "system": true, "idle": true, "type": true, "input": true, "bench": true, "suspend": true} + known := map[string]bool{"views": true, "wifi": true, "installer": true, "system": true, "idle": true, "type": true, "input": true, "bench": true, "suspend": true} for _, a := range apps { known[a.id] = true } @@ -620,6 +625,14 @@ func wakePointer(v *qemu.VM, _ *lf) { time.Sleep(2 * time.Second) } +// wakeLock wakes the display and stays still, as a person does who looks at the lock screen: the +// shell shows its hint ("Click or press a key") after 4 s without input (unlockDialog.js' +// HINT_TIMEOUT), and a view shot sooner had no word of how to unlock. +func wakeLock(v *qemu.VM, l *lf) { + wakePointer(v, l) + time.Sleep(4 * time.Second) +} + // wakeKey wakes the display with a first key; the second lifts the curtain to the prompt, and a // modifier alone does not. func wakeKey(v *qemu.VM, _ *lf) { @@ -715,8 +728,8 @@ var shellViews = []shellView{ `Shell.AppSystem.get_default().lookup_app("org.gnome.Settings.desktop").request_quit(); new imports.gi.Gio.Settings({schema_id: "org.gnome.desktop.interface"}).set_string("color-scheme", "prefer-dark")`, nil, 0, "Settings in the light scheme, the shell still dark: the window on the desk with its ring line, the sidebar the shelf, the selected row the sheet, one headerbar rule across both panes, no bright accent", []string{"S5", "S6", "C11", "E3", "C2"}, (*lf).viewSettingsLight}, - {"lock-screen", `Main.screenShield.lock(false)`, ``, wakePointer, 0, - "The lock screen on the boot's black, its clock regular, nothing of an extension in the panel", + {"lock-screen", `Main.screenShield.lock(false)`, ``, wakeLock, 0, + "The lock screen on the boot's black, its clock regular, the hint of how to unlock under it, nothing of an extension in the panel", []string{"S4", "T2"}, (*lf).viewLock}, {"lock-prompt", `Main.screenShield.showDialog()`, ``, wakeKey, 0, "The unlock prompt the shell's AuthPrompt draws: the entry, and the dialog's default its one primary action", diff --git a/cmd/image/wifi.go b/cmd/image/wifi.go new file mode 100644 index 0000000..ef47512 --- /dev/null +++ b/cmd/image/wifi.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/Construct-Linux/os/internal/needle" +) + +// wifiAPs is how many networks the wifi scenario puts in the air: more than the shell lists +// (network.js shows 8 and folds the rest into "More networks"), so a list that grows with the +// networks around shows here, as it did in a busy building where the menu went off the screen. +const wifiAPs = 12 + +// wifiSetup gives the machine Wi-Fi: mac80211_hwsim's radios, one the machine's own and the rest +// access points. NetworkManager is told to leave the access points alone, so the shell and +// Settings see one adapter and the networks it scans; wpa_supplicant's AP mode (CONFIG_AP, in its +// recipe) runs each network, WPA2 with a throwaway passphrase, each a unit of its own: a daemon +// the command started went with its SSH session, and the networks with it. +const wifiSetup = `set -e +modprobe mac80211_hwsim radios=%[1]d +for i in $(seq 1 50); do [ -e /sys/class/net/wlan%[2]d ] && break; sleep 0.2; done +mkdir -p /run/harness-wifi +for i in $(seq 1 %[2]d); do + until nmcli device set wlan$i managed no 2>/dev/null; do sleep 0.2; done + cat > /run/harness-wifi/$i.conf < GLib.timeout_add(GLib.PRIORITY_DEFAULT, 800, () => r())); + const t = lfAll(m.box, lfClass('quick-toggle-has-menu')).find(t => t.is_mapped() && t.constructor.name.includes('Wireless')); + t?.menu.open(true); + await new Promise(r => GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, () => r())); + const e = m.box.get_transformed_extents(); + const y = e.origin.y, h = e.size.height; + const items = t ? lfAll(t.menu.box, a => a.constructor.name.includes('NetworkItem') && a.is_mapped()).length : -1; + return {toggle: !!t, open: m.isOpen, mapped: m.actor.is_mapped(), y, h, stage: global.stage.height, items, + submenu: t?.menu.isOpen ?? false};` + +type wifiMenuSeen struct { + Toggle, Open, Mapped, Submenu bool + Y, H, Stage float64 + Items int +} + +// wifi is the scenario: the networks up, the machine scanning them, the shell's menu of them and +// Settings' Wi-Fi panel. +func (r *report) wifi(g *guest, l *lf) { + out, err := g.run(fmt.Sprintf(wifiSetup, wifiAPs+1, wifiAPs), "") + r.add("wifi access points", err == nil, fmt.Sprintf("%d networks by mac80211_hwsim%s%s", wifiAPs, lastLines(out, 4), errText(err))) + if err != nil { + return + } + var seenN int + ok := until(60*time.Second, 3*time.Second, func() bool { + s, _ := g.run(`nmcli -t -f SSID device wifi list ifname wlan0 --rescan yes`, "") + seenN = strings.Count(s, "Harness network") + return seenN >= wifiAPs + }) + r.add("wifi scan", ok, fmt.Sprintf("wlan0 sees %d of %d networks", seenN, wifiAPs)) + var s wifiMenuSeen + err = g.evalJSON(lfWifiMenu, &s) + time.Sleep(time.Second) + l.shot(viewMeta{"shell-quick-settings-wifi", needle.GroupShell, + "Quick settings with the Wi-Fi menu open among a dozen networks: the menu whole on the screen, the networks listed in the well", + []string{"S2", "E3"}}, nil) + r.add("wifi menu", err == nil && s.Toggle && s.Open && s.Mapped && s.Submenu && s.Items > 0 && s.Y >= 0 && s.Y+s.H <= s.Stage, + fmt.Sprintf("%+v%s", s, errText(err))) + _, _ = g.eval(lfPrelude + `const m = Main.panel.statusArea.quickSettings.menu; lfAll(m.box, lfClass('quick-toggle-has-menu')).forEach(t => t.menu?.close(false)); m.close(); true`) + time.Sleep(time.Second) + ok, detail := g.runPanel("wifi") + r.add("panel:wifi networks", ok, detail) + l.shot(viewMeta{"panel-wifi-networks", needle.GroupSettings, + "Settings' Wi-Fi panel with a dozen networks in range: the list's rows, their signal and lock", + []string{"S3", "TY4"}}, nil) + g.quitApp(settingsApp) +} From 5bd7ed03d5297dc8a7a175fb99f73ebd89f5b785 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 11:57:24 -0300 Subject: [PATCH 17/22] recipes: orchis-theme with a quick toggle menu's icons at the icon size A network's signal icon took St's default 48px in the Wi-Fi menu: with a dozen networks around the menu ran off the screen. Co-Authored-By: Claude Opus 5.5 --- recipes/orchis-theme.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/orchis-theme.yaml b/recipes/orchis-theme.yaml index bc9f50b..09754c0 100644 --- a/recipes/orchis-theme.yaml +++ b/recipes/orchis-theme.yaml @@ -1,7 +1,7 @@ package: name: orchis-theme version: "20260914" - epoch: 22 + epoch: 23 description: Orchis GTK and GNOME Shell theme in Construct's colors, light and dark copyright: - license: GPL-3.0-only @@ -28,7 +28,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/Orchis-theme branch: gnome-shell-51 - expected-commit: 41fd4d480067b3eb0332e4aae5cea0452e0c564f + expected-commit: 742d42005257d3b8fcaf5fa3b17f7d4371cfa63c # No -l: it links into a home directory, and the image links libadwaita per user. - runs: | From 1664a6fe7b9644726fa3e153038bc6a91614c0b7 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 12:04:17 -0300 Subject: [PATCH 18/22] image: the locks with orchis-theme r23 Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index f00bedf..5d4047b 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -245,8 +245,8 @@ contents: - openssh-sk-helper=10.6_p1-r1 - orca=51.1-r0 - orca-lang=51.1-r0 - - orchis-theme=20260914-r22 - - orchis-theme-shell=20260914-r22 + - orchis-theme=20260914-r23 + - orchis-theme-shell=20260914-r23 - ostree=2026.6-r0 - pam-u2f=1.4.0-r0 - pangomm=2.58.0-r0 From a5a036c29597684bbf63eb8c053d7c7b1a855a8d Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 12:04:17 -0300 Subject: [PATCH 19/22] recipes: clipboard-indicator flashes and colours as the theme On copy the panel button flashed an inline white block; it now takes the theme's pressed state, and the stylesheet's greys, white edges and 4px radii are the palette's and a control's 9px. Co-Authored-By: Claude Opus 5.5 --- recipes/gnome-shell-extension-clipboard-indicator.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/gnome-shell-extension-clipboard-indicator.yaml b/recipes/gnome-shell-extension-clipboard-indicator.yaml index 8027b53..5e0e516 100644 --- a/recipes/gnome-shell-extension-clipboard-indicator.yaml +++ b/recipes/gnome-shell-extension-clipboard-indicator.yaml @@ -1,7 +1,7 @@ package: name: gnome-shell-extension-clipboard-indicator version: "72" - epoch: 0 + epoch: 1 description: Clipboard history manager in the GNOME Shell top bar copyright: - license: MIT @@ -31,7 +31,7 @@ pipeline: with: repository: https://github.com/Construct-Linux/gnome-shell-extension-clipboard-indicator branch: gnome-51 - expected-commit: 2eaeafc2771d78eb56392136f0b69ea6812cdc55 + expected-commit: 0a286404eb6e1488feb7091edadde7b37e814e1b - runs: | glib-compile-schemas --strict --dry-run schemas From b484c1961684b85408d546036a731e3b46af4ff9 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 12:12:11 -0300 Subject: [PATCH 20/22] image: the locks with clipboard-indicator r1 Co-Authored-By: Claude Opus 5.5 --- image/apko.lock.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/image/apko.lock.yaml b/image/apko.lock.yaml index 5d4047b..b4d95bc 100644 --- a/image/apko.lock.yaml +++ b/image/apko.lock.yaml @@ -118,8 +118,8 @@ contents: - gnome-shell-extension-appindicator-lang=66-r6 - gnome-shell-extension-caffeine=61-r0 - gnome-shell-extension-caffeine-lang=61-r0 - - gnome-shell-extension-clipboard-indicator=72-r0 - - gnome-shell-extension-clipboard-indicator-lang=72-r0 + - gnome-shell-extension-clipboard-indicator=72-r1 + - gnome-shell-extension-clipboard-indicator-lang=72-r1 - gnome-shell-extension-dash-to-panel=74-r9 - gnome-shell-extension-drive-menu=51.0-r0 - gnome-shell-extension-gtk4-ding=100.29_git20261002-r3 From a20f61ea3cd8d35f3cd2144ea708f20d40839534 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 12:45:34 -0300 Subject: [PATCH 21/22] kernel: 7.3.3 Two stable point releases over 7.3.1. None of kernel/patches is in 7.3.2 or 7.3.3: each still applies, and the build stops on one that does not. Co-Authored-By: Claude Opus 5.5 --- build/versions.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/versions.env b/build/versions.env index 7dd4c56..038d233 100644 --- a/build/versions.env +++ b/build/versions.env @@ -7,8 +7,8 @@ WOLFI_BASE_IMAGE=cgr.dev/chainguard/wolfi-base:latest@sha256:88a20e2d017bd20cdc0 # The kernel, from kernel.org. A new series can fail the fragments' assertions; Arch's base config # (kernel/Dockerfile) is pinned to the same version, or Arch's newest while it has none for this one. -KERNEL_VERSION=7.3.1 -KERNEL_SHA256=fa595aad93734f829c963c2a90ef7c3673224fd7fdae7b49406e397cde2de231 +KERNEL_VERSION=7.3.3 +KERNEL_SHA256=79c66dde66cd96877fcde8f7d36aec946b1f423d2b97b5a7b1751b75c764f3d1 # Firmware (firmware/Dockerfile): linux-firmware's release, and Intel's microcode from its own. LINUX_FIRMWARE_VERSION=20261019 From 5de7236f60c9a3b75c579247523d000266208588 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Sun, 8 Nov 2026 13:47:43 -0300 Subject: [PATCH 22/22] image: the wifi scenario's access points unmanaged before they exist; the lock screen at 2 s NetworkManager set up all thirteen radios with its supplicant before the scenario told it to leave twelve alone, and under load it timed out on the machine's own (journal errors). A configuration in /run unmanages them from the start. Waiting for the lock screen's hint shot the display switched off: a few seconds after the pointer wakes it, the locked screen turns off again. The view is shot at 2 s, as before. Co-Authored-By: Claude Opus 5.5 --- cmd/image/test.go | 15 +++++---------- cmd/image/wifi.go | 8 +++++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/cmd/image/test.go b/cmd/image/test.go index 1723fd5..5660221 100644 --- a/cmd/image/test.go +++ b/cmd/image/test.go @@ -625,14 +625,6 @@ func wakePointer(v *qemu.VM, _ *lf) { time.Sleep(2 * time.Second) } -// wakeLock wakes the display and stays still, as a person does who looks at the lock screen: the -// shell shows its hint ("Click or press a key") after 4 s without input (unlockDialog.js' -// HINT_TIMEOUT), and a view shot sooner had no word of how to unlock. -func wakeLock(v *qemu.VM, l *lf) { - wakePointer(v, l) - time.Sleep(4 * time.Second) -} - // wakeKey wakes the display with a first key; the second lifts the curtain to the prompt, and a // modifier alone does not. func wakeKey(v *qemu.VM, _ *lf) { @@ -728,8 +720,11 @@ var shellViews = []shellView{ `Shell.AppSystem.get_default().lookup_app("org.gnome.Settings.desktop").request_quit(); new imports.gi.Gio.Settings({schema_id: "org.gnome.desktop.interface"}).set_string("color-scheme", "prefer-dark")`, nil, 0, "Settings in the light scheme, the shell still dark: the window on the desk with its ring line, the sidebar the shelf, the selected row the sheet, one headerbar rule across both panes, no bright accent", []string{"S5", "S6", "C11", "E3", "C2"}, (*lf).viewSettingsLight}, - {"lock-screen", `Main.screenShield.lock(false)`, ``, wakeLock, 0, - "The lock screen on the boot's black, its clock regular, the hint of how to unlock under it, nothing of an extension in the panel", + // Shot 2 s after the pointer wakes it: the shell's hint ("Click or press a key") comes after 4 s + // without input, and a few seconds later the locked screen turns off - a view that waited for + // the hint was the display switched off. + {"lock-screen", `Main.screenShield.lock(false)`, ``, wakePointer, 0, + "The lock screen on the boot's black, its clock regular, nothing of an extension in the panel", []string{"S4", "T2"}, (*lf).viewLock}, {"lock-prompt", `Main.screenShield.showDialog()`, ``, wakeKey, 0, "The unlock prompt the shell's AuthPrompt draws: the entry, and the dialog's default its one primary action", diff --git a/cmd/image/wifi.go b/cmd/image/wifi.go index ef47512..c7f6140 100644 --- a/cmd/image/wifi.go +++ b/cmd/image/wifi.go @@ -14,16 +14,18 @@ import ( const wifiAPs = 12 // wifiSetup gives the machine Wi-Fi: mac80211_hwsim's radios, one the machine's own and the rest -// access points. NetworkManager is told to leave the access points alone, so the shell and +// access points. NetworkManager is told to leave the access points alone before they exist - its +// supplicant set up all thirteen at once and timed out on the machine's own - so the shell and // Settings see one adapter and the networks it scans; wpa_supplicant's AP mode (CONFIG_AP, in its // recipe) runs each network, WPA2 with a throwaway passphrase, each a unit of its own: a daemon // the command started went with its SSH session, and the networks with it. const wifiSetup = `set -e +mkdir -p /run/NetworkManager/conf.d /run/harness-wifi +printf '[keyfile]\nunmanaged-devices=%%s\n' "$(seq -f 'interface-name:wlan%%g' -s ';' 1 %[2]d)" > /run/NetworkManager/conf.d/harness-wifi.conf +nmcli general reload conf modprobe mac80211_hwsim radios=%[1]d for i in $(seq 1 50); do [ -e /sys/class/net/wlan%[2]d ] && break; sleep 0.2; done -mkdir -p /run/harness-wifi for i in $(seq 1 %[2]d); do - until nmcli device set wlan$i managed no 2>/dev/null; do sleep 0.2; done cat > /run/harness-wifi/$i.conf <