package main import ( "bufio" "fmt" "os" "path/filepath" "strings" ) // Spec is one repository plus the options that belong to it - either a command // line invocation or one line of the config file. type Spec struct { Repo string Install string Name string Tag string Asset string Pattern string OS string Arch string Forge string Token string VersionFlag string Pre bool } func (s *Spec) set(key, val string) error { switch key { case "install": s.Install = val case "name": s.Name = val case "tag": s.Tag = val case "asset": s.Asset = val case "pattern": s.Pattern = val case "os": s.OS = val case "arch": s.Arch = val case "forge": s.Forge = val case "token": s.Token = val case "version-flag": s.VersionFlag = val case "pre": s.Pre = val != "" && val != "0" default: return fmt.Errorf("unknown key %q (allowed: %s)", key, strings.Join(specKeys, ", ")) } return nil } // Options typed on the command line win over the config file. Unlike the Perl // version this looks at what was actually given, so a "version-flag=" in the // config file is no longer overwritten by its own default. func (s *Spec) applyGlobals() { for _, k := range specKeys { if !given[k] { continue } var v string switch k { case "install": v = opt.install case "name": v = opt.name case "tag": v = opt.tag case "asset": v = opt.asset case "pattern": v = opt.pattern case "os": v = opt.os case "arch": v = opt.arch case "forge": v = opt.forge case "token": v = opt.token case "version-flag": v = opt.versionFlag case "pre": if !opt.pre { continue // --pre only ever switches on } v = "1" } s.set(k, v) } } func (s *Spec) versionFlag() string { if s.VersionFlag != "" { return s.VersionFlag } return opt.versionFlag } func (s *Spec) label() string { if s.Name != "" { return parseNames(s.Name, "?")[0].out } r := strings.TrimRight(s.Repo, "/") if r == "" { return "?" } return filepath.Base(r) } // ============================================================================== // Config file (--all) // ============================================================================== func configPath() string { base := os.Getenv("XDG_CONFIG_HOME") if base == "" { base = filepath.Join(homeDir(), ".config") } return filepath.Join(base, "upd", "tools") } // One entry per line: [key=value ...] [pre] func readConfig(path string) ([]*Spec, error) { fh, err := os.Open(path) if err != nil { return nil, fmt.Errorf("cannot read config %s: %w\n"+ "Format: one line per tool, e.g.\n"+ " https://github.com/sxyazi/yazi install=~/bin name=yazi,ya", path, err) } defer fh.Close() var specs []*Spec sc := bufio.NewScanner(fh) for ln := 1; sc.Scan(); ln++ { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } tokens := splitTokens(line) s := &Spec{Repo: tokens[0]} for _, t := range tokens[1:] { key, val, ok := strings.Cut(t, "=") if !ok { val = "1" // a bare "pre" } if err := s.set(strings.ToLower(key), val); err != nil { return nil, fmt.Errorf("%s:%d: %w", path, ln, err) } } s.applyGlobals() specs = append(specs, s) } if err := sc.Err(); err != nil { return nil, fmt.Errorf("cannot read config %s: %w", path, err) } return specs, nil } // Whitespace separated, with double quotes around values that contain spaces. func splitTokens(line string) []string { var out []string var cur strings.Builder quoted, escaped, started := false, false, false for _, r := range line { switch { case escaped: cur.WriteRune(r) escaped = false case r == '\\' && quoted: escaped = true case r == '"': quoted = !quoted started = true case (r == ' ' || r == '\t') && !quoted: if started { out = append(out, cur.String()) cur.Reset() started = false } default: cur.WriteRune(r) started = true } } if started { out = append(out, cur.String()) } return out } // ============================================================================== // Binary names // ============================================================================== // name holds what to look for inside the asset (src) and what to write to disk // (out): "yazi,ya" are two binaries, "yazi:yazi-nightly" renames one. type name struct{ src, out string } func parseNames(list, fallback string) []name { var names []name for _, part := range strings.Split(list, ",") { part = strings.TrimSpace(part) if part == "" { continue } src, out, ok := strings.Cut(part, ":") if !ok || out == "" { out = src } names = append(names, name{src: src, out: out}) } if len(names) == 0 { names = append(names, name{src: fallback, out: fallback}) } return names }