// Copyright (C) 2021 nytpu // SPDX-License-Identifier: AGPL-3.0-only // For more license details, see LICENSE or . // comitium is a feed aggregator for gemini. see README.md for more details and // doc/comitium.1.scd for a manual page. package main // generate the translations: //go:generate go-localize -input localizations_src -output localizations import ( "fmt" "net/url" "os" "path/filepath" "strconv" "github.com/mitchellh/go-homedir" "golang.nytpu.com/comitium/localizations" "golang.org/x/text/language" "golang.nytpu.com/comitium/core" "golang.nytpu.com/comitium/fetch" ) var supportedLangs = []language.Tag{ language.MustParse("en-US"), // default fallback language.MustParse("fr-FR"), language.MustParse("it-IT"), } func main() { // figure out what language to use (just getting $LANG if set since flags // aren't parsed yet) localizer := getLang() argCheck(os.Args, 2, true, localizer.Get("errors.no_command")) c, ok := commands[os.Args[1]] if !ok { fmt.Fprint(os.Stderr, core.TranslateStringLocalizer(localizer, "%s\n\n", "errors.bad_command", nil)) usage() os.Exit(1) } c.AddFlagSet(globalFs) c.Parse(os.Args[2:]) // figure out what language to use now that flags are parsed localizer = getLang() // these are special cases where we shouldn't try to touch data paths if os.Args[1] == "help" { usage() os.Exit(0) } if os.Args[1] == "version" { fmt.Printf(core.TranslateStringLocalizer( localizer, "%s\n", "errors.version", &localizations.Replacements{"version": core.Version, "commit": core.Commit}, )) os.Exit(0) } // dataPath is the location where all the data (list of subs, cached hashes, // etcetera) are stored dataPath := getDataPath(localizer) // make sure it exists so we can start working with files in it os.MkdirAll(dataPath, os.ModePerm) days, err := getHist() if err != nil { fmt.Fprint(os.Stderr, err) os.Exit(1) } err = core.Init(dataPath, localizer, days) if err != nil { fmt.Fprintf(os.Stderr, core.TranslateStringLocalizer( localizer, "%s\n", "errors.core_init", &localizations.Replacements{"error": err}, )) os.Exit(1) } switch os.Args[1] { case "refresh": argCheck(c.Args(), 0, false, localizer.Get("errors.no_argument", &localizations.Replacements{"command": "'refresh'"})) core.Timeout = *refreshTimeoutFlag fetch.RefreshAll(&core.Data, *refreshWorkersFlag) case "regenerate": argCheck(c.Args(), 0, false, localizer.Get("errors.no_argument", &localizations.Replacements{"command": "'regenerate'"})) case "add": argCheck(c.Args(), 1, true, localizer.Get("errors.no_url_add")) remote, err := url.Parse(c.Arg(0)) if err != nil { fmt.Fprintf(os.Stderr, core.TranslateString( "%s\n", "errors.url_parse", &localizations.Replacements{"error": err}, )) os.Exit(1) } if *addPageFlag { err = fetch.Page(&core.Data, remote, c.Arg(1)) } else { err = fetch.Feed(&core.Data, remote, c.Arg(1)) } if err != nil { fmt.Fprintf(os.Stderr, core.TranslateString( "%s\n", "errors.url_add", &localizations.Replacements{"error": err}, )) os.Exit(1) } case "remove": argCheck(c.Args(), 1, false, localizer.Get("errors.no_url_remove")) if (!*removeFeedsFlag && !*removePagesFlag) || *removeFeedsFlag { delete(core.Data.Feeds, c.Arg(0)) } if (!*removeFeedsFlag && !*removePagesFlag) || *removePagesFlag { delete(core.Data.Pages, c.Arg(0)) } } err = core.Data.WriteSubs(dataPath) if err != nil { fmt.Fprintf(os.Stderr, core.TranslateString( "%s\n", "errors.saving", &localizations.Replacements{"file": "subscriptions.gmi", "error": err}, )) os.Exit(1) } err = core.Data.WriteFeed(dataPath) if err != nil { fmt.Fprintf(os.Stderr, core.TranslateString( "%s\n", "errors.saving", &localizations.Replacements{"file": "feed.gmi", "error": err}, )) os.Exit(1) } err = core.Data.WriteJSON(dataPath) if err != nil { fmt.Fprintf(os.Stderr, core.TranslateString( "%s\n", "errors.saving", &localizations.Replacements{"file": "comtitium.json", "error": err}, )) os.Exit(1) } } // argCheck makes sure that the target number of arguments are given. If // not, msg is displayed alongside usage information and then the program exits // with failure. func argCheck(args []string, target int, lt bool, msg string) { if (lt && len(args) < target) || (!lt && len(args) != target) { fmt.Fprint(os.Stderr, msg, "\n\n") usage() os.Exit(1) } } // verifyPath will expand ~ if neccessary and will clean the path. func verifyPath(p string) string { if n, err := homedir.Expand(p); err != nil { return filepath.Clean(p) } else { return filepath.Clean(n) } } // getDataPath will return a data path in a gracefully degrading tree, picking // the first that exists: // --data flag, $COMITIUM_DATA, $XDG_DATA_HOME/comitium, // $HOME/.local/share/comitium, $HOME/.comitium, $PWD (last resort) func getDataPath(localizer *localizations.Localizer) string { if *dataFlag != "" { return verifyPath(*dataFlag) } if env := os.Getenv("COMITIUM_DATA"); env != "" { return verifyPath(env) } if env := os.Getenv("XDG_DATA_HOME"); env != "" { return verifyPath(env + "/comitium") } home, err := homedir.Dir() if err == nil { if _, err := os.Stat(verifyPath(home + "/.local")); !os.IsNotExist(err) { return verifyPath(home + "/.local/share/comitium") } return verifyPath(home + "/.comitium") } fmt.Fprintln(os.Stderr, localizer.Get("errors.no_data_dir")) return verifyPath("comitium") } // getLang will return the localizer for the user's lang func getLang() *localizations.Localizer { matcher := language.NewMatcher(supportedLangs) var lang language.Tag if *langFlag != "" { lang, _ = language.MatchStrings(matcher, *langFlag) } else if env := os.Getenv("LANG"); env != "" { lang, _ = language.MatchStrings(matcher, env) } return localizations.New(lang.String(), supportedLangs[0].String()) } func getHist() (days int, err error) { if env := os.Getenv("COMITIUM_HISTORY"); env != "" && *histFlag != 31 { return strconv.Atoi(env) } return *histFlag, nil } func usage() { fmt.Fprintf(os.Stderr, "Usage:\n") fmt.Fprintf(os.Stderr, " %v [FLAGS] [ARGUMENTS]\n\n", os.Args[0]) fmt.Fprint(os.Stderr, "Commands:\n") for _, v := range commandInfo { // FIXME: find a way to dynamically match the spacing that // flag.PrintDefaults() outputs (instead of hardcoding "-20") fmt.Fprintf(os.Stderr, " %-20s%s\n", v.Name, v.Usage) } fmt.Fprintf(os.Stderr, "\nGlobal Flags:\n%v", globalFs.FlagUsages()) // command-specific flags should be printed here fmt.Fprintf(os.Stderr, "Flags for \"add\":\n%v", commands["add"].FlagUsages()) fmt.Fprintf(os.Stderr, "Flags for \"refresh\":\n%v", commands["refresh"].FlagUsages()) fmt.Fprintf(os.Stderr, "Flags for \"remove\":\n%v\n", commands["remove"].FlagUsages()) fmt.Fprint(os.Stderr, "For more help, see comitium(1).\n") }