GTK 3/4 settings, qt6ct, portals.conf and the default cursor as owned files. GTK_THEME, GTK2_RC_FILES, the Qt words and XCURSOR_* as environment contributions. The GSettings keys the portal serves go in the xinitrc slot, replacing the predecessor's appearance script, and the cursor in the xresources slot. Qt is drawn by Fusion with qt6ct's darker palette instead of the user-repository adwaita-qt, which is no longer developed. qt5ct is dropped. The fonts are research 026's Inter and JetBrains Mono, and portals.conf routes the Secret interface to gnome-keyring, which nothing answered. The tools are appearance (dark or light per audience, switched for the session), cursor, icons, and portal-check (which backend answers which interface, and why).
247 lines
9.8 KiB
Go
247 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"adwaita/internal/desktop"
|
|
)
|
|
|
|
type manifest struct {
|
|
Capabilities []string `json:"capabilities"`
|
|
Claims []any `json:"claims"`
|
|
Tools []string `json:"tools"`
|
|
Environment struct {
|
|
Variables map[string]string `json:"variables"`
|
|
} `json:"environment"`
|
|
Shell []struct {
|
|
For, Slot, Code string
|
|
} `json:"shell"`
|
|
Resources []map[string]any `json:"resources"`
|
|
}
|
|
|
|
func readManifest(t *testing.T) manifest {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestItContributesTheThemesWordsAndClaimsNoSeat(t *testing.T) {
|
|
m := readManifest(t)
|
|
if m.Claims != nil {
|
|
t.Fatal("a theme is not a seat: several coexist")
|
|
}
|
|
want := map[string]string{"GTK_THEME": "Adwaita:dark", "GTK2_RC_FILES": "/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc",
|
|
"QT_QPA_PLATFORMTHEME": "qt6ct", "QT_STYLE_OVERRIDE": "Fusion", "QT_SELECT": "6", "XCURSOR_THEME": "Adwaita", "XCURSOR_SIZE": "24"}
|
|
if len(m.Environment.Variables) != len(want) {
|
|
t.Fatalf("%v", m.Environment.Variables)
|
|
}
|
|
for k, v := range want {
|
|
if m.Environment.Variables[k] != v {
|
|
t.Errorf("%s=%q", k, m.Environment.Variables[k])
|
|
}
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range tools(adwaita{}) {
|
|
served[tool.Name] = true
|
|
}
|
|
if len(served) != len(m.Tools) {
|
|
t.Fatalf("%v %v", served, m.Tools)
|
|
}
|
|
for _, n := range m.Tools {
|
|
if !served[n] {
|
|
t.Errorf("%s", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheSessionLinesSetGSettingsAndTheResourcesTheCursor(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Shell) != 2 || m.Shell[0].For != "xresources" || m.Shell[1].For != "xinitrc" || m.Shell[0].Slot != "normal" || m.Shell[1].Slot != "normal" {
|
|
t.Fatalf("%+v", m.Shell)
|
|
}
|
|
if m.Shell[0].Code != "! adwaita: the cursor, for X programs that take it from the resources.\nXcursor.theme: Adwaita\nXcursor.size: 24\n" {
|
|
t.Fatalf("%q", m.Shell[0].Code)
|
|
}
|
|
x := m.Shell[1].Code
|
|
for _, want := range []string{"color-scheme 'prefer-dark'", "gtk-theme 'Adwaita'", "icon-theme 'Adwaita'", "cursor-theme 'Adwaita'", "font-name 'Inter 11'", "monospace-font-name 'JetBrainsMono Nerd Font 11'"} {
|
|
if !strings.Contains(x, want) {
|
|
t.Errorf("lacks %s", want)
|
|
}
|
|
}
|
|
for _, l := range strings.Split(strings.TrimSpace(x), "\n") {
|
|
if !strings.HasPrefix(l, "#") && !strings.HasSuffix(l, "|| true") {
|
|
t.Errorf("a session line that can stop the session's start: %q", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestItOwnsTheFilesItsSourcesHoldAndNoQt5Duplicate(t *testing.T) {
|
|
m := readManifest(t)
|
|
sources := map[string]string{"gtk3": "gtk-settings.ini", "gtk4": "gtk-settings.ini", "qt6ct": "qt6ct.conf", "portals": "portals.conf", "cursor": "cursor-index.theme"}
|
|
pkgs := []string{}
|
|
for _, r := range m.Resources {
|
|
id := r["id"].(string)
|
|
if r["type"] == "package" {
|
|
pkgs = append(pkgs, r["package"].(string))
|
|
continue
|
|
}
|
|
raw, _ := os.ReadFile(filepath.Join("..", "..", "config", sources[id]))
|
|
if r["content"] != string(raw) || r["into"] != nil || r["owner"] != "${machine:account}" {
|
|
t.Errorf("%s is not config/%s, whole and the account's", id, sources[id])
|
|
}
|
|
if strings.Contains(r["path"].(string), "qt5ct") {
|
|
t.Error("qt5ct: both workstations run Qt 6 programs through qt6ct; a second tool is a duplicate")
|
|
}
|
|
}
|
|
if strings.Join(pkgs, ",") != "gnome-themes-extra,adwaita-icon-theme,adwaita-cursors,qt6ct,xdg-desktop-portal-gtk" {
|
|
t.Fatalf("%v", pkgs)
|
|
}
|
|
qt := readINI(filepath.Join("..", "..", "config", "qt6ct.conf"))
|
|
if qt["Appearance"]["style"] != "Fusion" || qt["Appearance"]["custom_palette"] != "true" || !strings.Contains(qt["Fonts"]["general"], "Inter,11") {
|
|
t.Fatalf("%v", qt)
|
|
}
|
|
if _, err := os.Stat("/usr/share/qt6ct/colors"); err == nil {
|
|
if _, err := os.Stat(qt["Appearance"]["color_scheme_path"]); err != nil {
|
|
t.Fatalf("qt6ct ships no %s", qt["Appearance"]["color_scheme_path"])
|
|
}
|
|
}
|
|
gtk := readINI(filepath.Join("..", "..", "config", "gtk-settings.ini"))["Settings"]
|
|
if gtk["gtk-theme-name"] != "Adwaita" || gtk["gtk-application-prefer-dark-theme"] != "1" || gtk["gtk-font-name"] != "Inter 11" {
|
|
t.Fatalf("%v", gtk)
|
|
}
|
|
}
|
|
|
|
func TestKeyFilesAreReadWithTheirComments(t *testing.T) {
|
|
ini := ParseINI("# c\n[A]\nk = v\n; also a comment\n[B]\nx=1=2\n")
|
|
if ini["A"]["k"] != "v" || ini["B"]["x"] != "1=2" || len(ini) != 2 {
|
|
t.Fatalf("%v", ini)
|
|
}
|
|
}
|
|
|
|
func TestThePortalsAnswerIsResolvedAsXdgDesktopPortalDoes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, "gtk.portal"), []byte("[portal]\nDBusName=org.freedesktop.impl.portal.desktop.gtk\nInterfaces=org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Settings;\nUseIn=gnome\n"), 0o644)
|
|
os.WriteFile(filepath.Join(dir, "gnome-keyring.portal"), []byte("[portal]\nDBusName=org.freedesktop.secrets\nInterfaces=org.freedesktop.impl.portal.Secret;\nUseIn=gnome\n"), 0o644)
|
|
os.WriteFile(filepath.Join(dir, "kde.portal"), []byte("[portal]\nDBusName=org.freedesktop.impl.portal.desktop.kde\nInterfaces=org.freedesktop.impl.portal.FileChooser;\nUseIn=KDE\n"), 0o644)
|
|
b := Backends([]string{dir})
|
|
if len(b) != 3 || b[0].Name != "gnome-keyring" || len(b[1].Interfaces) != 2 {
|
|
t.Fatalf("%+v", b)
|
|
}
|
|
got := Resolve(b, map[string]string{"default": "gtk", "org.freedesktop.impl.portal.Secret": "gnome-keyring"}, []string{"i3"})
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "gtk" || got["org.freedesktop.impl.portal.Secret"] != "gnome-keyring" || got["org.freedesktop.impl.portal.Settings"] != "gtk" {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
got = Resolve(b, map[string]string{"default": "gtk"}, []string{"i3"})
|
|
if got["org.freedesktop.impl.portal.Secret"] != "(none)" {
|
|
t.Fatalf("gtk does not implement secrets: %v", got)
|
|
}
|
|
got = Resolve(b, map[string]string{"default": "none;gtk"}, nil)
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "(none)" {
|
|
t.Fatalf("none stops the list: %v", got)
|
|
}
|
|
got = Resolve(b, nil, []string{"KDE"})
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "kde" || got["org.freedesktop.impl.portal.Settings"] != "(none)" {
|
|
t.Fatalf("with no configuration, UseIn decides: %v", got)
|
|
}
|
|
c := PortalConfigs("/h", []string{"i3", "GNOME"})
|
|
if c[0] != "/h/.config/xdg-desktop-portal/i3-portals.conf" || c[1] != "/h/.config/xdg-desktop-portal/gnome-portals.conf" || c[2] != "/h/.config/xdg-desktop-portal/portals.conf" {
|
|
t.Fatalf("%v", c[:3])
|
|
}
|
|
}
|
|
|
|
func TestThemesAreFoundOnceEachWithCursorsAndIcons(t *testing.T) {
|
|
a, b := t.TempDir(), t.TempDir()
|
|
os.MkdirAll(filepath.Join(a, "Adwaita", "cursors"), 0o755)
|
|
os.MkdirAll(filepath.Join(b, "Adwaita"), 0o755)
|
|
os.WriteFile(filepath.Join(b, "Adwaita", "index.theme"), []byte("[Icon Theme]\nName=Adwaita\nInherits=hicolor\nDirectories=16x16\n"), 0o644)
|
|
os.MkdirAll(filepath.Join(b, "hicolor"), 0o755)
|
|
os.WriteFile(filepath.Join(b, "hicolor", "index.theme"), []byte("[Icon Theme]\nName=Hicolor\nDirectories=16x16\n"), 0o644)
|
|
os.MkdirAll(filepath.Join(b, "empty"), 0o755)
|
|
got := Themes([]string{a, b})
|
|
if len(got) != 2 || got[0].Name != "Adwaita" || !got[0].Cursors || got[0].Icons || got[0].Dir != filepath.Join(a, "Adwaita") || got[1].Name != "hicolor" {
|
|
t.Fatalf("the first directory's Adwaita hides the second's: %+v", got)
|
|
}
|
|
}
|
|
|
|
type fake struct {
|
|
ran []string
|
|
get map[string]string
|
|
}
|
|
|
|
func (f *fake) desk() desktop.Desk {
|
|
return desktop.Desk{
|
|
Find: func() (*desktop.Session, error) { return nil, &desktop.NoSession{Reason: "none"} },
|
|
Run: func(_ context.Context, env []string, _ []byte, name string, args ...string) desktop.Result {
|
|
line := strings.Join(append([]string{name}, args...), " ")
|
|
f.ran = append(f.ran, line)
|
|
switch {
|
|
case name == "gsettings" && args[0] == "get":
|
|
return desktop.Result{Stdout: "'" + f.get[args[2]] + "'\n"}
|
|
case name == "gsettings" && args[0] == "set":
|
|
f.get[args[2]] = args[3]
|
|
case name == "systemctl":
|
|
return desktop.Result{Stdout: "GTK_THEME=Adwaita:dark\nNPM_TOKEN=secret\nXDG_CURRENT_DESKTOP=i3\n"}
|
|
case name == "busctl" && args[1] == "call":
|
|
return desktop.Result{Stdout: "v u 1\n"}
|
|
}
|
|
return desktop.Result{}
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAppearanceSwitchesGSettingsAndSaysWhatFollowsAndWhatStays(t *testing.T) {
|
|
f := &fake{get: map[string]string{"color-scheme": "prefer-dark", "gtk-theme": "Adwaita"}}
|
|
a := adwaita{d: f.desk(), home: t.TempDir()}
|
|
got, err := a.appearance(context.Background(), desktop.Args{"mode": "light"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j := asJSON(got)
|
|
if f.get["color-scheme"] != "prefer-light" || !strings.Contains(j, `"switched":"light"`) || !strings.Contains(j, "GTK_THEME=Adwaita:dark") || !strings.Contains(j, `"lasts"`) {
|
|
t.Fatalf("%s", j)
|
|
}
|
|
if strings.Contains(j, "secret") || strings.Contains(j, "NPM_TOKEN") {
|
|
t.Fatal("only the theme's words are read back from the user manager")
|
|
}
|
|
if _, err := a.appearance(context.Background(), desktop.Args{"mode": "blue"}); err == nil {
|
|
t.Fatal("mode is dark or light")
|
|
}
|
|
got, _ = a.appearance(context.Background(), desktop.Args{})
|
|
if strings.Contains(asJSON(got), "switched") {
|
|
t.Fatal("reading changes nothing")
|
|
}
|
|
}
|
|
|
|
func TestACursorThemeMustBeInstalledAndWithoutASessionOnlyGSettingsChanges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.MkdirAll(filepath.Join(dir, "Adwaita", "cursors"), 0o755)
|
|
f := &fake{get: map[string]string{}}
|
|
a := adwaita{d: f.desk(), home: t.TempDir(), iconDirs: []string{dir}}
|
|
if _, err := a.cursor(context.Background(), desktop.Args{"theme": "Bibata"}); err == nil {
|
|
t.Fatal("a theme that is not installed")
|
|
}
|
|
got, err := a.cursor(context.Background(), desktop.Args{"theme": "Adwaita", "size": float64(32)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.get["cursor-theme"] != "Adwaita" || f.get["cursor-size"] != "32" || !strings.Contains(asJSON(got), "no graphical session") {
|
|
t.Fatalf("%v %s", f.get, asJSON(got))
|
|
}
|
|
}
|
|
|
|
func asJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|