diff --git a/README.md b/README.md index 8b267a9..78cecc4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ I am using the api of [diyhrt.market](https://diyhrt.market/api/) to get the current stats data of estrogen stocks. -# Installation +## Installation To install the new tab page you can use `go install` @@ -13,11 +13,23 @@ go install gitea.elara.ws/Hazel/transfem-startpage Then you can run the program `transfem-startpage` ```sh -transfem-startpage +transfem-startpage help ``` To configure this new tab page as website, you can install the firefox extension [New Tab Override](https://addons.mozilla.org/en-US/firefox/addon/new-tab-override/). Then just configure the url as `http://127.0.0.1:{port}/`. The default port should be `5500` but it will also print it out when starting the server. Make sure to check the box `Set focus to the web page instead of the address bar` in the extension settings, because the new tab page auto focuses the search bar. +## CLI + +```sh +transfem-startpage {program} {...args} +``` + +program | args | description +---|---|--- +`help` | `program:optional` | get more information on how the cli or one program works +`start` | `profile:optional` | start the webserver for a certain profile +`cache` | `action:emum(clear;clean)` | so something with the cache + ## Config and Profiles This tool works with profiles. The default profile is `default`. If you want to load another profile just write it as command line arg after the command. To write a config File you can create the files here: diff --git a/go.mod b/go.mod index 4b7cafc..fc0c587 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.2 require github.com/labstack/echo/v4 v4.13.4 require ( + github.com/TwiN/go-color v1.4.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index ada8512..4748c27 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= +github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..fed52cb --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,98 @@ +package cache + +import ( + "crypto/sha1" + "encoding/hex" + "errors" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/utils" + "github.com/labstack/echo/v4" +) + +type Cache struct { + CacheDir string + Disabled bool +} + +func getCacheDir() (string, error) { + baseDir, err := os.UserCacheDir() + if err != nil { + baseDir = "/tmp" + } + cacheDir := filepath.Join(baseDir, utils.Name) + err = os.MkdirAll(cacheDir, 0o755) + if err != nil { + return "", err + } + return cacheDir, nil +} + +func getProfileCacheDir(profile string) (string, error) { + var profileCacheDir string + + cacheDir, err := getCacheDir() + if err != nil { + return profileCacheDir, err + } + + profileCacheDir = filepath.Join(cacheDir, profile) + err = os.MkdirAll(cacheDir, 0o755) + return profileCacheDir, err +} + +func NewCache(profile string) Cache { + cacheDir, err := getProfileCacheDir(profile) + + return Cache{ + CacheDir: cacheDir, + Disabled: err != nil, + } +} + +const baseCacheUrl = "cache" + +func (c Cache) StartStaticServer(e *echo.Echo) error { + e.Static("/"+baseCacheUrl, c.CacheDir) + return nil +} + +func hashUrl(url string) string { + h := sha1.New() + io.WriteString(h, url) + return hex.EncodeToString(h.Sum(nil)) +} + +func (c Cache) CacheUrl(urlString string) (string, error) { + filename := hashUrl(urlString) + filepath.Ext(urlString) + targetPath := filepath.Join(c.CacheDir, filename) + + // if the file was already downloaded it doesn't need to be downloaded again + if _, err := os.Stat(targetPath); errors.Is(err, os.ErrNotExist) { + resp, err := http.Get(urlString) + if !errors.Is(err, os.ErrNotExist) { + return urlString, err + } + defer resp.Body.Close() + + file, err := os.Create(targetPath) + if err != nil { + return urlString, err + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + + if err != nil { + return urlString, err + } + } else { + return url.JoinPath(baseCacheUrl, filename) + } + + return url.JoinPath(baseCacheUrl, filename) +} diff --git a/internal/cli/cache.go b/internal/cli/cache.go new file mode 100644 index 0000000..4951e09 --- /dev/null +++ b/internal/cli/cache.go @@ -0,0 +1,9 @@ +package cli + +import "log" + +func Cache() error { + log.Println("running cache") + log.Panicln("not implemented yet") + return nil +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..d76479a --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,102 @@ +package cli + +import ( + "log" + "os" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/utils" + "github.com/TwiN/go-color" +) + +type ProgramFunction func() error +type Program struct { + Name string + Function ProgramFunction + ShortDescription string + LongDescription string + Arguments []Argument +} +type Argument struct { + Name string + Type string + Required bool + Description string +} + +var HelpHeader = `This is the help page of ` + utils.Name + `. +` + color.Purple + utils.BinaryName + ` {program} {...args}` + color.Reset + ` +The following Programs are available:` +var Programs = []Program{ + { + Name: "help", + ShortDescription: "get more information on how the cli in general or a specific program works", + LongDescription: "What did you expect to find here?", + Arguments: []Argument{ + { + Name: "program", + Type: "string", + Required: false, + Description: "defines the program you want to know more about", + }, + }, + }, + { + Name: "start", + Function: Start, + ShortDescription: "start the webserver", + LongDescription: `The start program starts the webserver. +It loads the config file of the according profile. +It uses the default values if no config file was found.`, + Arguments: []Argument{ + { + Name: "profile", + Type: "string", + Required: false, + Description: "tells the program which config to load, default is 'default'", + }, + }, + }, + { + Name: "cache", + Function: Cache, + ShortDescription: "do something with the cache", + LongDescription: `Does something with the cache. +- clear: delete the whole cache +- clean: delete all files that aren't used by any program.`, + Arguments: []Argument{ + { + Name: "action", + Type: "enum(clear;clean)", + Required: true, + Description: "defines what to do with the cache", + }, + }, + }, +} + +func GetProgram(programName string) Program { + for i, p := range Programs { + if p.Name == programName { + return Programs[i] + } + } + + log.Panicln("couldn't find program", programName, ". EXITING") + return Program{} +} + +func Cli() { + // getting around initialization cycle + Programs[0].Function = Help + + programName := "help" + if len(os.Args) > 1 { + programName = os.Args[1] + } + + var selectedProgram Program = GetProgram(programName) + err := selectedProgram.Function() + if err != nil { + log.Panicln(err) + } +} diff --git a/internal/cli/help.go b/internal/cli/help.go new file mode 100644 index 0000000..876b0c3 --- /dev/null +++ b/internal/cli/help.go @@ -0,0 +1,90 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/utils" + "github.com/TwiN/go-color" +) + +func padString(s string, n int) string { + missing := n - len(s) + if missing <= 0 { + return s + } + + for _ = range missing { + s = s + " " + } + + return s +} + +func getSingleArgumentString(a Argument) string { + requiredString := "" + if a.Required { + requiredString = "*" + } + return requiredString + a.Name + ":" + a.Type +} + +func getArgumentString(arguments []Argument) string { + argumentString := color.Blue + for _, a := range arguments { + argumentString = argumentString + " [" + getSingleArgumentString(a) + "]" + } + return argumentString + color.Reset +} + +func generalHelp() error { + fmt.Println() + fmt.Println(HelpHeader) + fmt.Println() + + for _, p := range Programs { + fmt.Print(color.Bold + padString(p.Name, 7) + color.Reset) + fmt.Print(padString(getArgumentString(p.Arguments), 40) + p.ShortDescription + "\n") + } + return nil +} + +func specificHelp(programName string) error { + program := GetProgram(programName) + + fmt.Println(color.Bold + "MAN PAGE FOR " + strings.ToUpper(programName) + color.Reset) + fmt.Println() + + fmt.Println(color.Purple + utils.BinaryName + " " + programName + color.Reset + getArgumentString(program.Arguments)) + fmt.Println() + + fmt.Println(color.Bold + "arguments" + color.Reset) + + argumentStrings := make([]string, len(program.Arguments)) + maxArgumentString := 0 + for i, a := range program.Arguments { + s := getSingleArgumentString(a) + argumentStrings[i] = s + if len(s) > maxArgumentString { + maxArgumentString = len(s) + } + } + + for i, a := range program.Arguments { + fmt.Println(padString(argumentStrings[i], maxArgumentString+4) + a.Description) + } + + fmt.Println() + fmt.Println(program.LongDescription) + + return nil +} + +func Help() error { + if len(os.Args) > 2 { + return specificHelp(os.Args[2]) + } + + return generalHelp() +} diff --git a/internal/cli/start.go b/internal/cli/start.go new file mode 100644 index 0000000..8b813a1 --- /dev/null +++ b/internal/cli/start.go @@ -0,0 +1,18 @@ +package cli + +import ( + "log" + "os" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/server" +) + +func Start() error { + profile := "default" + if len(os.Args) > 2 { + profile = os.Args[2] + } + log.Println("starting server with profile " + profile) + + return server.Start(profile) +} diff --git a/internal/diyhrt/fetch.go b/internal/diyhrt/fetch.go index d2c9fb6..d1db765 100644 --- a/internal/diyhrt/fetch.go +++ b/internal/diyhrt/fetch.go @@ -11,7 +11,7 @@ const endpoint = "https://diyhrt.market/api/listings" func GetListings(apiKey string) ([]Listing, error) { if apiKey == "" { - return nil, errors.New("API_KEY key not set. Set it as env or in DiyHrt.ApiKey") + return nil, errors.New("diyhrt API_KEY key not set. Set it as env or in DiyHrt.ApiKey") } // Create HTTP client diff --git a/internal/rendering/config.go b/internal/rendering/config.go index ec56829..7e56f8f 100644 --- a/internal/rendering/config.go +++ b/internal/rendering/config.go @@ -2,13 +2,12 @@ package rendering import ( "errors" - "fmt" - "maps" + "log" "os" "path/filepath" - "slices" "gitea.elara.ws/Hazel/transfem-startpage/internal/diyhrt" + "gitea.elara.ws/Hazel/transfem-startpage/internal/utils" "github.com/pelletier/go-toml" ) @@ -102,23 +101,12 @@ func NewConfig() Config { } } -func (c *Config) LoadDiyHrt(listings []diyhrt.Listing) { - existingStores := make(map[int]diyhrt.Store) - - for _, listing := range listings { - existingStores[listing.Store.Id] = listing.Store - } - - c.Template.Listings = c.DiyHrt.ListingFilter.Filter(listings) - c.Template.Stores = c.DiyHrt.StoreFilter.Filter(slices.Collect(maps.Values(existingStores))) -} - func (rc *Config) ScanForConfigFile(profile string) error { profileFile := profile + ".toml" baseDir, cacheDirErr := os.UserConfigDir() if cacheDirErr == nil { - configFile := filepath.Join(baseDir, "startpage", profileFile) + configFile := filepath.Join(baseDir, utils.Name, profileFile) if err := rc.LoadConfigFile(configFile); !errors.Is(err, os.ErrNotExist) { return err @@ -141,7 +129,7 @@ func (rc *Config) LoadConfigFile(file string) error { return err } - fmt.Println("loading config file: " + file) + log.Println("loading config file", file) content, err := os.ReadFile(file) @@ -151,14 +139,3 @@ func (rc *Config) LoadConfigFile(file string) error { return toml.Unmarshal(content, rc) } - -func (c *Config) Init() error { - fmt.Print("downloading website icons") - for i := range c.Template.Websites { - fmt.Print(".") - c.Template.Websites[i].Cache() - } - fmt.Print("\n") - - return nil -} diff --git a/internal/rendering/diyhrt.go b/internal/rendering/diyhrt.go new file mode 100644 index 0000000..9c94aac --- /dev/null +++ b/internal/rendering/diyhrt.go @@ -0,0 +1,28 @@ +package rendering + +import ( + "maps" + "slices" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/diyhrt" +) + +func (c *Config) LoadDiyHrt(listings []diyhrt.Listing) { + existingStores := make(map[int]diyhrt.Store) + + for _, listing := range listings { + existingStores[listing.Store.Id] = listing.Store + } + + c.Template.Listings = c.DiyHrt.ListingFilter.Filter(listings) + c.Template.Stores = c.DiyHrt.StoreFilter.Filter(slices.Collect(maps.Values(existingStores))) +} + +func (c *Config) FetchDiyHrt() error { + l, err := diyhrt.GetListings(c.DiyHrt.ApiKey) + if err != nil { + return err + } + c.LoadDiyHrt(l) + return nil +} diff --git a/internal/server/embed.go b/internal/server/embed.go new file mode 100644 index 0000000..a1dfc13 --- /dev/null +++ b/internal/server/embed.go @@ -0,0 +1,42 @@ +package server + +import ( + "bytes" + "embed" + "io/fs" + "log" + "net/http" + "text/template" + + "github.com/labstack/echo/v4" +) + +var FrontendFiles embed.FS + +func getFileContent() string { + content, err := FrontendFiles.ReadFile("frontend/index.html") + + if err != nil { + log.Fatal(err) + } + + return string(content) +} + +func getIndex(c echo.Context) error { + IndexTemplate := template.Must(template.New("index").Parse(getFileContent())) + + var tpl bytes.Buffer + IndexTemplate.Execute(&tpl, Config.Template) + + return c.HTML(http.StatusOK, tpl.String()) +} + +func getFileSystem() http.FileSystem { + fsys, err := fs.Sub(FrontendFiles, "frontend") + if err != nil { + panic(err) + } + + return http.FS(fsys) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..a14fa28 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,52 @@ +package server + +import ( + "log" + "net/http" + "strconv" + + "gitea.elara.ws/Hazel/transfem-startpage/internal/cache" + "gitea.elara.ws/Hazel/transfem-startpage/internal/rendering" + "github.com/labstack/echo/v4" +) + +var Config = rendering.NewConfig() + +func Start(profile string) error { + err := Config.ScanForConfigFile(profile) + if err != nil { + return err + } + + err = Config.FetchDiyHrt() + if err != nil { + log.Println(err) + } + + e := echo.New() + + // statically serve the file + cache := cache.NewCache(profile) + if !cache.Disabled { + cache.StartStaticServer(e) + + log.Println("downloading website icons...") + for i, w := range Config.Template.Websites { + u, err := cache.CacheUrl(w.ImageUrl) + if err != nil { + log.Println(err) + } + Config.Template.Websites[i].ImageUrl = u + Config.Template.Websites[i].IsFetched = true + } + } + + // https://echo.labstack.com/docs/cookbook/embed-resources + staticHandler := http.FileServer(getFileSystem()) + e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/", staticHandler))) + e.GET("/scripts/*", echo.WrapHandler(http.StripPrefix("/", staticHandler))) + e.GET("/", getIndex) + + e.Logger.Fatal(e.Start(":" + strconv.Itoa(Config.Server.Port))) + return nil +} diff --git a/internal/utils/meta.go b/internal/utils/meta.go new file mode 100644 index 0000000..133bc73 --- /dev/null +++ b/internal/utils/meta.go @@ -0,0 +1,6 @@ +package utils + +import "os" + +var Name = "transfem-startpage" +var BinaryName = os.Args[0] diff --git a/main.go b/main.go index 96f3d47..0bb0db3 100644 --- a/main.go +++ b/main.go @@ -1,102 +1,16 @@ package main import ( - "bytes" "embed" - "fmt" - "html/template" - "io/fs" - "log" - "net/http" - "os" - "strconv" - "gitea.elara.ws/Hazel/transfem-startpage/internal/diyhrt" - "gitea.elara.ws/Hazel/transfem-startpage/internal/rendering" - "github.com/labstack/echo/v4" + "gitea.elara.ws/Hazel/transfem-startpage/internal/cli" + "gitea.elara.ws/Hazel/transfem-startpage/internal/server" ) -var CurrentConfig = rendering.NewConfig() - -func FetchDiyHrt() error { - fmt.Println("Fetch DiyHrt Marketplaces...") - - l, err := diyhrt.GetListings(CurrentConfig.DiyHrt.ApiKey) - if err != nil { - return err - } - CurrentConfig.LoadDiyHrt(l) - return nil -} - //go:embed frontend/* -var frontendFiles embed.FS - -func getFileContent() string { - content, err := frontendFiles.ReadFile("frontend/index.html") - - if err != nil { - log.Fatal(err) - } - - return string(content) -} - -var IndexTemplate = template.Must(template.New("index").Parse(getFileContent())) - -func getIndex(c echo.Context) error { - var tpl bytes.Buffer - IndexTemplate.Execute(&tpl, CurrentConfig.Template) - - return c.HTML(http.StatusOK, tpl.String()) -} - -func getFileSystem() http.FileSystem { - fsys, err := fs.Sub(frontendFiles, "frontend") - if err != nil { - panic(err) - } - - return http.FS(fsys) -} +var FrontendFiles embed.FS func main() { - profile := "default" - if len(os.Args) > 1 { - profile = os.Args[1] - } - fmt.Println("loading profile " + profile) - - err := CurrentConfig.ScanForConfigFile(profile) - if err != nil { - fmt.Println(err) - } - - err = CurrentConfig.Init() - if err != nil { - fmt.Println(err) - } - - err = FetchDiyHrt() - if err != nil { - fmt.Println(err) - } - - e := echo.New() - - // statically serve the file - cacheDir, err := rendering.GetCacheDir() - if err == nil { - e.Static("/cache", cacheDir) - } else { - fmt.Println(err) - } - - // https://echo.labstack.com/docs/cookbook/embed-resources - staticHandler := http.FileServer(getFileSystem()) - e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/", staticHandler))) - e.GET("/scripts/*", echo.WrapHandler(http.StripPrefix("/", staticHandler))) - e.GET("/", getIndex) - - e.Logger.Fatal(e.Start(":" + strconv.Itoa(CurrentConfig.Server.Port))) + server.FrontendFiles = FrontendFiles + cli.Cli() } diff --git a/tmp/build-errors.log b/tmp/build-errors.log index 9ba0172..a2a130a 100644 --- a/tmp/build-errors.log +++ b/tmp/build-errors.log @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file