Compare commits

...

14 Commits

Author SHA1 Message Date
70c668bdf1 Merge pull request 'cli' (#1) from cli into main
Reviewed-on: #1
2025-07-16 14:35:52 +00:00
Hazel Noack
68c89de1a4 made some metadata of the program dynamic 2025-07-16 16:25:22 +02:00
Hazel Noack
0b7de76874 edited readme 2025-07-16 16:13:48 +02:00
Hazel Noack
797e115191 rewrote cache to work better 2025-07-16 14:24:42 +02:00
Hazel Noack
16a65df664 rewrote ftm to log 2025-07-16 12:37:58 +02:00
Hazel Noack
4e87c77ccb implemented start server 2025-07-16 12:31:51 +02:00
Hazel Noack
94b3f4c0f2 implemented start server 2025-07-16 12:31:47 +02:00
Hazel Noack
f574a00a8f not implemented cache 2025-07-16 12:09:33 +02:00
Hazel Noack
5bce49eed4 implemented specific help 2025-07-16 12:08:53 +02:00
Hazel Noack
51b788bb50 finished general help 2025-07-16 11:45:28 +02:00
Hazel Noack
7f43eb43e0 added arguments data structure 2025-07-16 11:27:32 +02:00
Hazel Noack
81b960c231 implemented general help 2025-07-16 11:20:58 +02:00
Hazel Noack
26f1008dc4 implemented basic program structure 2025-07-16 11:09:18 +02:00
Hazel Noack
f6558e3cd7 layed out the basics on how 2025-07-16 10:41:36 +02:00
16 changed files with 473 additions and 122 deletions

View File

@ -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:

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

98
internal/cache/cache.go vendored Normal file
View File

@ -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)
}

9
internal/cli/cache.go Normal file
View File

@ -0,0 +1,9 @@
package cli
import "log"
func Cache() error {
log.Println("running cache")
log.Panicln("not implemented yet")
return nil
}

102
internal/cli/cli.go Normal file
View File

@ -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)
}
}

90
internal/cli/help.go Normal file
View File

@ -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()
}

18
internal/cli/start.go Normal file
View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

42
internal/server/embed.go Normal file
View File

@ -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)
}

52
internal/server/server.go Normal file
View File

@ -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
}

6
internal/utils/meta.go Normal file
View File

@ -0,0 +1,6 @@
package utils
import "os"
var Name = "transfem-startpage"
var BinaryName = os.Args[0]

96
main.go
View File

@ -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()
}

View File

@ -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
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