diff --git a/Makefile b/Makefile index 9239b50..610e5af 100644 --- a/Makefile +++ b/Makefile @@ -12,20 +12,20 @@ CONFIG_MARKER := $(CONFIG_DIR)/.tint-installed-config STATE_MARKER := $(CONFIG_DIR)/.tint-installed-state .PHONY: install uninstall -install: +install: default-state.toml config.toml @command -v tinty >/dev/null || { echo "tinty is required" >&2; exit 1; } @command -v dark-notify >/dev/null || { echo "dark-notify is required" >&2; exit 1; } mkdir -p "$(BIN_DIR)" "$(CONFIG_DIR)" "$(HERDR_DIR)" "$(LAUNCH_AGENT_DIR)" "$(dir $(DARK_NOTIFY_LOG))" go build -o "$(BIN_DIR)/tint" . - @if [ "$(CURDIR)" != "$(CONFIG_DIR)" ]; then \ + @set -e; if [ "$(CURDIR)" != "$(CONFIG_DIR)" ]; then \ if [ -e "$(CONFIG_MARKER)" ] || [ ! -e "$(CONFIG_DIR)/config.toml" ]; then \ cp config.toml "$(CONFIG_DIR)/config.toml"; \ touch "$(CONFIG_MARKER)"; \ fi; \ - if [ ! -e "$(CONFIG_DIR)/state.toml" ]; then \ - cp state.toml "$(CONFIG_DIR)/state.toml"; \ - touch "$(STATE_MARKER)"; \ - fi; \ + fi; \ + if [ ! -e "$(CONFIG_DIR)/state.toml" ]; then \ + cp default-state.toml "$(CONFIG_DIR)/state.toml"; \ + touch "$(STATE_MARKER)"; \ fi @if [ ! -e "$(HERDR_CONFIG)" ]; then \ touch "$(HERDR_CONFIG)" "$(HERDR_MARKER)"; \ diff --git a/README.md b/README.md index 3874fdd..9c7b0f5 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The installer: - Builds `~/.local/bin/tint`. - Installs `config.toml` under `$XDG_CONFIG_HOME/tinted-theming/tinty`, or `~/.config/tinted-theming/tinty` when `XDG_CONFIG_HOME` is unset. -- Copies `state.toml` only when no state file already exists. +- Copies the versioned `default-state.toml` to the installed `state.toml` only when no state file already exists. - Creates `~/.config/herdr/config.toml` if needed. - Runs `tinty sync` to download the configured theme repositories. - Creates and loads a `dark-notify` LaunchAgent for automatic macOS appearance switching. @@ -180,7 +180,7 @@ After changing or updating the wrapper source: make install ``` -Existing `state.toml` content is preserved. +Existing `state.toml` content is preserved. Missing state is recreated from `default-state.toml`, including the bundled named pairs and favorites. The source directory's ignored `state.toml` is personal state and is not used by the installer. ## Uninstall diff --git a/default-state.toml b/default-state.toml new file mode 100644 index 0000000..a1917d7 --- /dev/null +++ b/default-state.toml @@ -0,0 +1,104 @@ +[current] +light = "base16-swamp-light" +dark = "base16-darktooth" + +[[favorites.pairs]] +name = "gruvbox-material-medium" +dark = "base16-gruvbox-material-dark-medium" +light = "base16-gruvbox-material-light-medium" + +[[favorites.pairs]] +name = "gruvbox-pale" +dark = "base16-gruvbox-dark-pale" +light = "base16-gruvbox-light-soft" + +[[favorites.pairs]] +name = "swamp" +dark = "base16-swamp-dark" +light = "base16-swamp-light" + +[favorites] +themes = [ +"base16-0x96f", +"base16-arroz-con-dulce", +"base16-ascendancy", +"base16-atelier-plateau-light", +"base16-cacao", +"base16-caroline", +"base16-chinoiserie-midnight", +"base16-corduroy", +"base16-cupcake", +"base16-darktooth", +"base16-embers", +"base16-gruvbox-dark-pale", +"base16-pastelon-de-amarillos-dark", +"base16-precious-light-warm", +"base16-rose-pine-dawn", +"base16-selenized-light", +"base16-swamp-dark", +"base16-swamp-light", +"base24-ic-orange-ppl", +"base24-later-this-evening", +"tinted8-gruvbox-dark", +] + +[[others.pairs]] +name = "amarillos" +dark = "base16-pastelon-de-amarillos-dark" +light = "base16-pastelon-de-amarillos" + +[[others.pairs]] +name = "charcoal" +dark = "base16-charcoal-dark" +light = "base16-charcoal-light" + +[[others.pairs]] +name = "embers" +dark = "base16-embers" +light = "base16-embers-light" + +[[others.pairs]] +name = "gruvbox-material-hard" +dark = "base16-gruvbox-material-dark-hard" +light = "base16-gruvbox-material-light-hard" + +[[others.pairs]] +name = "gruvbox-material-soft" +dark = "base16-gruvbox-material-dark-soft" +light = "base16-gruvbox-material-light-soft" + +[[others.pairs]] +name = "pale-swamp" +dark = "base16-gruvbox-dark-pale" +light = "base16-swamp-light" + +[others] +themes = [ +"base16-apprentice", +"base16-ashes", +"base16-atelier-heath", +"base16-atelier-heath-light", +"base16-black-metal", +"base16-black-metal-bathory", +"base16-black-metal-khold", +"base16-black-metal-mayhem", +"base16-chinoiserie-morandi", +"base16-corduroy-dark", +"base16-da-one-gray", +"base16-default-dark", +"base16-equilibrium-light", +"base16-helios", +"base16-kanagawa-dragon", +"base16-mezcal", +"base16-railscasts", +"base16-sandcastle", +"base16-tarot", +"base16-vesper", +"base16-zenbones", +"base24-alucard", +"base24-earthsong", +"base24-flexoki-light", +"base24-mountain", +"base24-pastelon-de-amarillos", +"base24-sleepy-hollow", +] diff --git a/install_test.go b/install_test.go new file mode 100644 index 0000000..7e4eb9a --- /dev/null +++ b/install_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestInstallFromFreshCheckout(t *testing.T) { + tmp := t.TempDir() + source := filepath.Join(tmp, "source") + bin := filepath.Join(tmp, "bin") + for _, dir := range []string{source, bin} { + if err := os.Mkdir(dir, 0o755); err != nil { + t.Fatal(err) + } + } + entries, err := os.ReadDir(".") + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if !entry.Type().IsRegular() || entry.Name() == "state.toml" { + continue + } + data, err := os.ReadFile(entry.Name()) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(source, entry.Name()), data, 0o644); err != nil { + t.Fatal(err) + } + } + writeExecutable(t, filepath.Join(bin, "tinty"), `#!/bin/sh +case "$1" in + sync|info) exit 0 ;; + apply) printf '%s\n' "$2" >> "$TINTY_LOG" ;; + *) exit 2 ;; +esac +`) + for _, name := range []string{"dark-notify", "launchctl"} { + writeExecutable(t, filepath.Join(bin, name), "#!/bin/sh\nexit 0\n") + } + home := filepath.Join(tmp, "home") + config := filepath.Join(home, ".config") + log := filepath.Join(tmp, "applied") + cache, err := exec.Command("go", "env", "GOMODCACHE").Output() + if err != nil { + t.Fatal(err) + } + t.Setenv("GOMODCACHE", strings.TrimSpace(string(cache))) + t.Setenv("PATH", bin+":"+os.Getenv("PATH")) + t.Setenv("TINTY_LOG", log) + install := func() { + t.Helper() + cmd := exec.Command("make", "install", "HOME="+home, "XDG_CONFIG_HOME="+config) + cmd.Dir = source + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("install failed: %v\n%s", err, output) + } + } + install() + statePath := filepath.Join(config, "tinted-theming", "tinty", "state.toml") + value, err := loadState(statePath) + if err != nil { + t.Fatalf("fresh install did not provide usable state: %v", err) + } + cmd := exec.Command(filepath.Join(home, ".local", "bin", "tint"), "swamp") + cmd.Env = append(os.Environ(), "TINT_STATE_FILE="+statePath, "TINTY_PAIR_APPEARANCE=dark") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("selecting an installed pair failed: %v\n%s", err, output) + } + if got := strings.TrimSpace(readFile(t, log)); got != "base16-swamp-dark" { + t.Fatalf("applied theme = %q, want base16-swamp-dark", got) + } + value.Current = currentPair{Light: "personal-light", Dark: "personal-dark"} + if err := saveState(statePath, value); err != nil { + t.Fatal(err) + } + before, err := os.ReadFile(statePath) + if err != nil { + t.Fatal(err) + } + install() + after, err := os.ReadFile(statePath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Fatal("reinstall overwrote personal state") + } +}