254 lines
6.8 KiB
Go
254 lines
6.8 KiB
Go
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
|
|
if err := cmd.Run(); err == nil {
|
|
return nil
|
|
}
|
|
cmd = exec.Command("tinty", "info", theme, "--custom-schemes")
|
|
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)
|
|
}
|