Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fe8cba6
fork: brand's files in a fork come from the brand commit the image pins
aledbf Nov 8, 2026
e18dfa8
recipes: the design review's first round, and palettes from one brand
aledbf Nov 8, 2026
ab001c0
image: the locks at the design review's first round
aledbf Nov 8, 2026
e2164c6
image: -only leaves the session's checks out, and theme-dev takes apps
aledbf Nov 8, 2026
4aad1db
installer: no disk is a state with a way out
aledbf Nov 8, 2026
d677ca0
recipes: the design review's second round
aledbf Nov 8, 2026
04ec8da
orchis-theme: Files' selected place at the pane's edge
aledbf Nov 8, 2026
334b26d
image: the locks at the design review's second round
aledbf Nov 8, 2026
940c79a
construct-app: a page's title is said once
aledbf Nov 8, 2026
94d4870
recipes: the design review's third round
aledbf Nov 8, 2026
8ed5123
image: the locks at the design review's third round
aledbf Nov 8, 2026
7108214
recipes: orchis-theme for the design review's fourth round
aledbf Nov 8, 2026
b95b4aa
image: the locks at the design review's fourth round
aledbf Nov 8, 2026
5c4c037
image: lf checks on as the on tint with text
aledbf Nov 8, 2026
af8060f
image: tilingshell's border is the brand's cyan
aledbf Nov 8, 2026
c4e1000
image: test Wi-Fi among a dozen networks, the lock screen with its hint
aledbf Nov 8, 2026
5bd7ed0
recipes: orchis-theme with a quick toggle menu's icons at the icon size
aledbf Nov 8, 2026
1664a6f
image: the locks with orchis-theme r23
aledbf Nov 8, 2026
a5a036c
recipes: clipboard-indicator flashes and colours as the theme
aledbf Nov 8, 2026
b484c19
image: the locks with clipboard-indicator r1
aledbf Nov 8, 2026
a20f61e
kernel: 7.3.3
aledbf Nov 8, 2026
5de7236
image: the wifi scenario's access points unmanaged before they exist;…
aledbf Nov 8, 2026
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
9 changes: 7 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions build/versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions cmd/fork/brand.go
Original file line number Diff line number Diff line change
@@ -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 <recipe>) 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
}
102 changes: 102 additions & 0 deletions cmd/fork/brand_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
13 changes: 13 additions & 0 deletions cmd/fork/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion cmd/fork/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// XML parse, metadata declares GNOME Shell 51 alone, the recipe's patches apply (cmd/fork/check.go)
// fork pin <recipe> 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 <recipe> 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>/<repository>, beside this repository by default,
Expand Down Expand Up @@ -35,7 +37,8 @@ const usage = `usage:
fork sync <recipe> [-ours] [-recipes dir] [-forks dir] [-out dir]
fork check <recipe> [-recipes dir] [-forks dir]
fork pin <recipe> [-recipes dir] [-forks dir] [-out dir]
fork list [-recipes dir]`
fork list [-recipes dir]
fork brand <recipe> [-recipes dir] [-forks dir]`

// where is what every command is told: the recipes, the checkouts and the reports.
type where struct {
Expand Down Expand Up @@ -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)
}
Expand Down
18 changes: 18 additions & 0 deletions cmd/image/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -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:<name> or views: %s", strings.Join(unknown, ", "))
}
if *orchis == "" {
return errors.New("theme-dev: -orchis names an Orchis-theme checkout (ORCHIS)")
}
Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Loading