back up project

This commit is contained in:
hl-valdemar
2026-09-18 00:21:11 +02:00
commit ffe8b986ce
10 changed files with 1322 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
package main
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
)
var pairNamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
type usageError struct{}
func (usageError) Error() string { return "invalid arguments" }
func main() {
if err := runCLI(os.Args[1:], os.Stdout, os.Stderr); err != nil {
var usage usageError
if errors.As(err, &usage) {
printHelp(os.Stderr, defaultStatePath())
os.Exit(2)
}
fmt.Fprintf(os.Stderr, "tint: %v\n", err)
os.Exit(1)
}
}
func runCLI(args []string, stdout, stderr io.Writer) error {
statePath := defaultStatePath()
if len(args) == 2 && args[0] == "apply-apps" {
return applyApps(args[1])
}
if len(args) == 0 {
printHelp(stdout, statePath)
return nil
}
if len(args) == 1 {
switch args[0] {
case "-h", "--help":
printHelp(stdout, statePath)
return nil
case "config":
data, err := os.ReadFile(statePath)
if err != nil {
return err
}
_, err = stdout.Write(data)
return err
case "light", "dark":
value, err := loadState(statePath)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
if err := validatePair(value.Current, stderr); err != nil {
return err
}
return applySelected(value.Current, args[0], stdout, stderr)
}
if mode, theme, ok := parseThemeArg(args[0]); ok {
value, err := loadState(statePath)
if err != nil {
return usageError{}
}
if mode == "light" {
value.Current.Light = theme
} else {
value.Current.Dark = theme
}
return updateAndApply(statePath, value, "", stdout, stderr)
}
value, err := loadState(statePath)
if err != nil {
return err
}
named, _, found := value.Catalog.findPair(args[0])
if !found {
return fmt.Errorf("unknown pair: %s", args[0])
}
value.Current = currentPair{Light: named.Light, Dark: named.Dark}
return updateAndApply(statePath, value, "", stdout, stderr)
}
if len(args) == 2 {
if args[0] == "favorite" || args[0] == "unfavorite" {
value, err := loadState(statePath)
if err != nil {
return err
}
_, _, isPair := value.Catalog.findPair(args[1])
if !isPair && args[0] == "favorite" {
if err := validateTheme(args[1], stderr); err != nil {
return err
}
}
if args[0] == "favorite" {
value.Catalog.favorite(args[1])
} else {
value.Catalog.unfavorite(args[1])
}
return saveState(statePath, value)
}
firstMode, firstTheme, firstOK := parseThemeArg(args[0])
secondMode, secondTheme, secondOK := parseThemeArg(args[1])
if !firstOK || !secondOK || firstMode == secondMode {
return usageError{}
}
value, err := loadState(statePath)
if errors.Is(err, os.ErrNotExist) {
value = state{}
} else if err != nil {
return err
}
if firstMode == "light" {
value.Current.Light, value.Current.Dark = firstTheme, secondTheme
} else {
value.Current.Dark, value.Current.Light = firstTheme, secondTheme
}
return updateAndApply(statePath, value, "", stdout, stderr)
}
if len(args) == 4 && args[0] == "pair" {
if !pairNamePattern.MatchString(args[1]) || reservedPairName(args[1]) {
return fmt.Errorf("invalid pair name: %s", args[1])
}
darkMode, dark, darkOK := parseThemeArg(args[2])
lightMode, light, lightOK := parseThemeArg(args[3])
if !darkOK || !lightOK || darkMode != "dark" || lightMode != "light" {
return usageError{}
}
if err := validatePair(currentPair{Light: light, Dark: dark}, stderr); err != nil {
return err
}
value, err := loadState(statePath)
if err != nil {
return err
}
value.Catalog.configurePair(themePair{Name: args[1], Dark: dark, Light: light})
return saveState(statePath, value)
}
return usageError{}
}
func parseThemeArg(arg string) (string, string, bool) {
mode, theme, ok := strings.Cut(arg, ":")
if !ok || theme == "" || (mode != "light" && mode != "dark") {
return "", "", false
}
return mode, theme, true
}
func reservedPairName(name string) bool {
switch name {
case "config", "light", "dark", "pair", "favorite", "unfavorite", "help", "-h", "--help", "apply-apps":
return true
default:
return false
}
}
func updateAndApply(path string, value state, requested string, stdout, stderr io.Writer) error {
if err := validatePair(value.Current, stderr); err != nil {
return err
}
value.Catalog.record(value.Current)
if err := saveState(path, value); err != nil {
return err
}
var err error
if requested == "" {
requested, err = currentAppearance()
if err != nil {
return err
}
}
return applySelected(value.Current, requested, stdout, stderr)
}
func validatePair(pair currentPair, stderr io.Writer) error {
if pair.Light == "" || pair.Dark == "" {
return usageError{}
}
if err := validateTheme(pair.Light, stderr); err != nil {
return err
}
return validateTheme(pair.Dark, stderr)
}
func validateTheme(theme string, stderr io.Writer) error {
cmd := exec.Command("tinty", "info", theme)
cmd.Stdout = io.Discard
cmd.Stderr = stderr
return cmd.Run()
}
func currentAppearance() (string, error) {
if appearance := os.Getenv("TINTY_PAIR_APPEARANCE"); appearance != "" {
if appearance != "light" && appearance != "dark" {
return "", fmt.Errorf("TINTY_PAIR_APPEARANCE must be light or dark")
}
return appearance, nil
}
output, err := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle").Output()
if err == nil && strings.TrimSpace(string(output)) == "Dark" {
return "dark", nil
}
return "light", nil
}
func applySelected(pair currentPair, appearance string, stdout, stderr io.Writer) error {
theme := pair.Light
if appearance == "dark" {
theme = pair.Dark
}
cmd := exec.Command("tinty", "apply", theme)
cmd.Stdout = stdout
cmd.Stderr = stderr
return cmd.Run()
}
func printCurrent(writer io.Writer, path string) {
value, err := loadState(path)
if err != nil {
value.Current = currentPair{Light: "-", Dark: "-"}
}
fmt.Fprintf(writer, "[current]\nlight:%s\ndark:%s\n\n", value.Current.Light, value.Current.Dark)
}
func printHelp(writer io.Writer, statePath string) {
io.WriteString(writer, `Usage:
tint Show this help
tint config Print the configuration
tint favorite <theme|pair> Add a theme or pair to favorites
tint unfavorite <theme|pair> Remove a theme or pair from favorites
tint <pair> Switch to a saved pair
tint pair <name> dark:<theme> light:<theme>
Save a named theme pair
tint light|dark Apply the saved light or dark theme
tint light:<theme> Set the light theme
tint dark:<theme> Set the dark theme
tint light:<theme> dark:<theme> Set both themes
`)
printCurrent(writer, statePath)
}