package config
import (
"fmt"
"log/slog"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/flohoss/godash/pkg/media"
"github.com/go-playground/validator/v10"
"github.com/spf13/viper"
)
const (
ConfigFolder = "./config/"
iconsFolder = ConfigFolder + "icons/"
)
var cfg GlobalConfig
var validate *validator.Validate
var mu sync.RWMutex
var reloadMu sync.Mutex
var (
iconCache = map[string][2]string{}
iconCacheMu sync.Mutex
)
type GlobalConfig struct {
LogLevel string `mapstructure:"log_level" validate:"omitempty,oneof=debug info warn error"`
TimeZone string `mapstructure:"time_zone" validate:"omitempty,timezone"`
Title string `mapstructure:"title"`
Server ServerSettings `mapstructure:"server"`
Weather Weather `mapstructure:"weather"`
Applications []Category `mapstructure:"applications"`
Links []Category `mapstructure:"links"`
}
type ServerSettings struct {
Address string `mapstructure:"address" validate:"omitempty,ipv4"`
Port int `mapstructure:"port" validate:"omitempty,gte=1024,lte=65535"`
}
type Weather struct {
Units string `mapstructure:"units" validate:"omitempty,oneof=celsius fahrenheit"`
Latitude float64 `mapstructure:"latitude" validate:"omitempty,latitude"`
Longitude float64 `mapstructure:"longitude" validate:"omitempty,longitude"`
}
type Category struct {
Category string `mapstructure:"category"`
Entries []App `mapstructure:"entries"`
}
type App struct {
Name string `mapstructure:"name"`
Icon string `mapstructure:"icon"`
IconLight string `mapstructure:"-"`
URL string `mapstructure:"url" validate:"omitempty,url"`
IgnoreDark bool `mapstructure:"ignore_dark"`
}
type AppConfig struct {
Name string `mapstructure:"name"`
Icon string `mapstructure:"icon"`
URL string `mapstructure:"url"`
IgnoreDark bool `mapstructure:"ignore_dark"`
}
func init() {
os.Mkdir(ConfigFolder, 0750)
os.Mkdir(iconsFolder, 0750)
validate = validator.New()
}
func New() {
viper.SetDefault("log_level", "info")
viper.SetDefault("time_zone", "Europe/Berlin")
viper.SetDefault("server.address", "0.0.0.0")
viper.SetDefault("server.port", 8156)
viper.SetDefault("title", "GoDash")
viper.SetDefault("weather.units", "celsius")
viper.SetDefault("weather.latitude", 52.5163)
viper.SetDefault("weather.longitude", 13.3776)
viper.SetDefault("applications", []map[string]any{
{
"category": "Applications",
"entries": []map[string]any{
{
"name": "GoDash",
"icon": "sh/homebox",
"url": "https://github.com/flohoss/godash",
},
},
},
})
viper.SetDefault("links", []map[string]any{
{
"category": "Applications",
"entries": []map[string]any{
{
"name": "GoDash",
"url": "https://github.com/flohoss/godash",
},
},
},
})
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(ConfigFolder)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err = viper.WriteConfigAs(ConfigFolder + "config.yaml")
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
} else {
slog.Error("Failed to read configuration file", "error", err)
os.Exit(1)
}
}
if err := ValidateAndLoadConfig(); err != nil {
slog.Error("Initial configuration validation failed", "error", err)
os.Exit(1)
}
}
func ValidateAndLoadConfig() error {
reloadMu.Lock()
defer reloadMu.Unlock()
var tempCfg GlobalConfig
if err := viper.Unmarshal(&tempCfg); err != nil {
return fmt.Errorf("failed to unmarshal configuration: %w", err)
}
if err := validate.Struct(tempCfg); err != nil {
return fmt.Errorf("configuration validation failed: %w", err)
}
clearIconCache()
replaceIconStrings(tempCfg.Applications)
replaceIconStrings(tempCfg.Links)
cleanupOrphanedIcons(iconsFolder, tempCfg.Applications, tempCfg.Links)
mu.Lock()
cfg = tempCfg
mu.Unlock()
return nil
}
func replaceIconStrings(applications []Category) {
for i := range applications {
for j := range applications[i].Entries {
bookmark := &applications[i].Entries[j]
if cached, ok := lookupIconCache(bookmark.Icon); ok {
bookmark.Icon = cached[0]
bookmark.IconLight = cached[1]
continue
}
var filePath, filePathLight string
switch {
case isExternalIcon(bookmark.Icon):
if u, err := url.Parse(bookmark.Icon); err == nil && u.Scheme != "" && u.Host != "" {
filePath, filePathLight = bookmark.Icon, ""
}
case strings.HasPrefix(bookmark.Icon, "sh/"):
path, lightPath, err := downloadSelfHostedIcon(bookmark.Icon)
if err != nil {
slog.Error(err.Error())
} else {
filePath, filePathLight = path, lightPath
}
default:
ext := filepath.Ext(bookmark.Icon)
path, lightPath, err := handleLocalIcons(iconsFolder, bookmark.Icon, ext)
if err != nil {
slog.Warn("could not find local icon", "path", bookmark.Icon, "error", err)
}
filePath, filePathLight = path, lightPath
}
storeIconCache(bookmark.Icon, [2]string{filePath, filePathLight})
bookmark.Icon = filePath
bookmark.IconLight = filePathLight
}
}
}
func lookupIconCache(key string) ([2]string, bool) {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
v, ok := iconCache[key]
return v, ok
}
func storeIconCache(key string, value [2]string) {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
iconCache[key] = value
}
func clearIconCache() {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
iconCache = map[string][2]string{}
}
func cleanupOrphanedIcons(iconsDir string, categories ...[]Category) {
referenced := map[string]bool{}
for _, apps := range categories {
for i := range apps {
for _, app := range apps[i].Entries {
for _, path := range []string{app.Icon, app.IconLight} {
if path == "" || isExternalIcon(path) {
continue
}
referenced[path] = true
}
}
}
}
entries, err := os.ReadDir(iconsDir)
if err != nil {
slog.Warn("could not scan icons folder for cleanup", "error", err)
return
}
for _, entry := range entries {
path := "/icons/" + entry.Name()
if !referenced[path] {
if err := os.Remove(iconsDir + entry.Name()); err != nil {
slog.Warn("could not remove orphaned icon", "path", entry.Name(), "error", err)
} else {
slog.Info("removed orphaned icon", "path", entry.Name())
}
}
}
}
func isExternalIcon(icon string) bool {
return strings.HasPrefix(icon, "http://") || strings.HasPrefix(icon, "https://")
}
func downloadSelfHostedIcon(icon string) (string, string, error) {
for _, ext := range []string{".svg", ".webp", ".png"} {
title, iconURL, lightTitle, lightURL := handleSelfHostedIcons(icon, ext)
if title == "" {
return "", "", fmt.Errorf("invalid self-hosted icon: %s", icon)
}
path, lightPath, err := downloadIcons(title, iconURL, lightTitle, lightURL)
if err == nil {
return path, lightPath, nil
}
slog.Debug("self-hosted icon format unavailable, trying next", "icon", icon, "ext", ext, "error", err)
}
return "", "", fmt.Errorf("no available format for self-hosted icon: %s", icon)
}
func downloadIcons(title, url, lightTitle, lightUrl string) (string, string, error) {
path, err := downloadIcon(title, url)
if err != nil {
return "", "", err
}
lightPath, err := downloadIcon(lightTitle, lightUrl)
if err != nil {
slog.Warn("could not download light icon, falling back to base", "title", lightTitle, "error", err)
lightPath = path
}
return path, lightPath, nil
}
func downloadIcon(title, url string) (string, error) {
filePath := iconsFolder + title
_, err := os.Stat(filePath)
if os.IsNotExist(err) {
filePath, err = media.DownloadSelfHostedIcon(url, title, filePath)
if err != nil {
return "", err
}
} else if err != nil {
return "", fmt.Errorf("stat icon %q: %w", filePath, err)
}
return "/" + strings.TrimPrefix(filePath, ConfigFolder), nil
}
func handleSelfHostedIcons(icon, ext string) (string, string, string, string) {
name := strings.Replace(icon, "sh/", "", 1)
clean := filepath.Clean(name)
if strings.Contains(clean, "..") || filepath.IsAbs(clean) {
return "", "", "", ""
}
title := clean + ext
encoded := url.PathEscape(title)
iconURL := "https://cdn.jsdelivr.net/gh/selfhst/icons/" + strings.TrimPrefix(ext, ".") + "/" + encoded
lightTitle := strings.Replace(title, ext, "-light"+ext, 1)
lightEncoded := url.PathEscape(lightTitle)
lightURL := "https://cdn.jsdelivr.net/gh/selfhst/icons/" + strings.TrimPrefix(ext, ".") + "/" + lightEncoded
return title, iconURL, lightTitle, lightURL
}
func handleLocalIcons(iconsDir, title, ext string) (string, string, error) {
clean := filepath.Clean(title)
if strings.Contains(clean, "..") || filepath.IsAbs(clean) {
return "", "", fmt.Errorf("invalid icon path: %s", title)
}
filePath := iconsDir + clean
if _, err := os.Stat(filePath); err != nil {
return "", "", err
}
basePath := "/" + strings.TrimPrefix(filePath, ConfigFolder)
filePathLight := strings.Replace(filePath, ext, "-light"+ext, 1)
if _, err := os.Stat(filePathLight); err != nil {
return basePath, basePath, nil
}
return basePath, "/" + strings.TrimPrefix(filePathLight, ConfigFolder), nil
}
func ConfigLoaded() bool {
return viper.ConfigFileUsed() != ""
}
func GetLogLevel() slog.Level {
mu.RLock()
defer mu.RUnlock()
switch strings.ToLower(cfg.LogLevel) {
case "debug":
return slog.LevelDebug
case "warn", "warning":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
func GetServer() string {
mu.RLock()
defer mu.RUnlock()
return net.JoinHostPort(cfg.Server.Address, strconv.Itoa(cfg.Server.Port))
}
func GetApplications() []Category {
mu.RLock()
defer mu.RUnlock()
return cfg.Applications
}
func GetLinks() []Category {
mu.RLock()
defer mu.RUnlock()
return cfg.Links
}
func GetTitle() string {
mu.RLock()
defer mu.RUnlock()
return cfg.Title
}
func GetTimeZone() string {
mu.RLock()
defer mu.RUnlock()
return cfg.TimeZone
}
func GetWeatherConfig() (Weather, string) {
mu.RLock()
defer mu.RUnlock()
return cfg.Weather, cfg.TimeZone
}
package handlers
import (
"github.com/flohoss/godash/config"
"github.com/flohoss/godash/services"
"github.com/flohoss/godash/views"
"github.com/labstack/echo/v5"
)
type SystemService interface {
GetBuffer() services.Buffer
GetStatic() services.Static
}
type WeatherService interface {
GetCurrentWeather() []services.Day
GetCurrentHourly() []services.Hour
}
func NewAppHandler(s SystemService, w WeatherService) *AppHandler {
return &AppHandler{
systemService: s,
weatherService: w,
}
}
type AppHandler struct {
systemService SystemService
weatherService WeatherService
}
func (bh *AppHandler) handleIndex(ctx *echo.Context) error {
buffer := bh.systemService.GetBuffer()
static := bh.systemService.GetStatic()
weather := bh.weatherService.GetCurrentWeather()
hourly := bh.weatherService.GetCurrentHourly()
return render(ctx, views.Home(config.GetTitle(), config.GetApplications(), config.GetLinks(), buffer, static, weather, hourly))
}
package handlers
import (
"bytes"
"net/http"
"sync"
"github.com/a-h/templ"
"github.com/labstack/echo/v5"
"github.com/r3labs/sse/v2"
)
func longCacheLifetime(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
return next(c)
}
}
func sseConnectionLimiter(maxPerIP int) echo.MiddlewareFunc {
var mu sync.Mutex
counts := map[string]int{}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
ip := c.RealIP()
mu.Lock()
if counts[ip] >= maxPerIP {
mu.Unlock()
return c.NoContent(http.StatusServiceUnavailable)
}
counts[ip]++
mu.Unlock()
defer func() {
mu.Lock()
counts[ip]--
if counts[ip] <= 0 {
delete(counts, ip)
}
mu.Unlock()
}()
return next(c)
}
}
}
func render(c *echo.Context, cmp templ.Component) error {
var buf bytes.Buffer
if err := cmp.Render(c.Request().Context(), &buf); err != nil {
return c.String(http.StatusInternalServerError, "render error")
}
w := c.Response()
w.Header().Set(echo.HeaderContentType, "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
return nil
}
func SetupRoutes(e *echo.Echo, sse *sse.Server, appHandler *AppHandler) {
e.GET("/sse", echo.WrapHandler(sse), sseConnectionLimiter(10))
assets := e.Group("/assets", longCacheLifetime)
assets.Static("/", "assets")
icons := e.Group("/icons", longCacheLifetime)
icons.Static("/", "config/icons")
e.GET("/robots.txt", func(ctx *echo.Context) error {
return ctx.String(http.StatusOK, "User-agent: *\nDisallow: /")
})
e.GET("/", appHandler.handleIndex)
e.GET("/*", func(c *echo.Context) error {
return c.Redirect(http.StatusFound, "/")
})
}
package readable
import "fmt"
const (
KiB uint64 = 1024
MiB = KiB * 1024
GiB = MiB * 1024
TiB = GiB * 1024
PiB = TiB * 1024
EiB = PiB * 1024
)
func amountString(size uint64) (uint64, string) {
switch {
case size < KiB:
return 1, "B"
case size < MiB:
return KiB, "KiB"
case size < GiB:
return MiB, "MiB"
case size < TiB:
return GiB, "GiB"
case size < PiB:
return TiB, "TiB"
case size < EiB:
return PiB, "PiB"
default:
return EiB, "EiB"
}
}
func ReadableSizeWithUnit(size uint64, unit uint64) float64 {
return float64(size) / float64(unit)
}
func ReadableSize(size uint64) string {
unit, unitStr := amountString(size)
return fmt.Sprintf("%.2f %s", ReadableSizeWithUnit(size, unit), unitStr)
}
func ReadableSizePair(size1, size2 uint64) string {
maxSize := size1
if size2 > size1 {
maxSize = size2
}
unit, unitStr := amountString(maxSize)
return fmt.Sprintf("%.2f / %.2f %s",
ReadableSizeWithUnit(size1, unit),
ReadableSizeWithUnit(size2, unit),
unitStr,
)
}
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
sseserver "github.com/r3labs/sse/v2"
"github.com/spf13/viper"
"github.com/flohoss/godash/config"
"github.com/flohoss/godash/handlers"
"github.com/flohoss/godash/services"
)
func setupRouter(logger *slog.Logger) *echo.Echo {
e := echo.NewWithConfig(echo.Config{
Logger: logger,
IPExtractor: echo.ExtractIPFromRealIPHeader(),
})
e.Use(middleware.RequestID())
e.Use(middleware.Recover())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c *echo.Context) bool {
return c.Path() == "/sse"
},
}))
return e
}
func setLogger() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: config.GetLogLevel(),
}))
slog.SetDefault(logger)
slog.Debug("logger set", "level", config.GetLogLevel())
}
func setupViperWatcher(echoInst *echo.Echo) {
var (
mu sync.Mutex
timer *time.Timer
)
debounce := func(d time.Duration, fn func()) {
mu.Lock()
defer mu.Unlock()
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(d, fn)
}
viper.OnConfigChange(func(e fsnotify.Event) {
debounce(2*time.Second, func() {
config.ValidateAndLoadConfig()
setLogger()
echoInst.Logger = slog.Default()
slog.Debug("config changed", "file", e.Name)
})
})
viper.WatchConfig()
}
func main() {
config.New()
setLogger()
e := setupRouter(slog.Default())
setupViperWatcher(e)
sse := sseserver.New()
sse.AutoReplay = false
sse.OnSubscribe = func(streamID string, sub *sseserver.Subscriber) {
services.PublishSnapshot(streamID)
}
s := services.NewSystemService(sse)
w := services.NewWeatherService(sse)
appHandler := handlers.NewAppHandler(s, w)
handlers.SetupRoutes(e, sse, appHandler)
slog.Info("Starting server", "url", fmt.Sprintf("http://%s", config.GetServer()))
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
go s.Run(ctx)
go w.Run(ctx)
sc := echo.StartConfig{
Address: config.GetServer(),
HideBanner: true,
HidePort: true,
GracefulTimeout: 10 * time.Second,
BeforeServeFunc: func(s *http.Server) error {
s.ReadHeaderTimeout = 10 * time.Second
s.ReadTimeout = 30 * time.Second
s.IdleTimeout = 120 * time.Second
return nil
},
}
if err := sc.Start(ctx, e); err != nil {
slog.Error("Failed to start server", "error", err)
}
}
package media
import (
"fmt"
"io"
"io/fs"
"net/http"
"os"
"strings"
"time"
)
var httpClient = &http.Client{Timeout: 10 * time.Second}
func DownloadSelfHostedIcon(url, title, filePath string) (string, error) {
resp, err := httpClient.Get(url)
if err != nil {
return "", fmt.Errorf("failed to get icon: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get icon, status: %d, url: %s", resp.StatusCode, url)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
if err != nil {
return "", fmt.Errorf("failed to read icon: %w", err)
}
if !isIconContentType(resp.Header.Get("Content-Type"), data) {
return "", fmt.Errorf("downloaded icon is not an image: %s", http.DetectContentType(data))
}
tmpPath := filePath + ".tmp"
if err := os.WriteFile(tmpPath, data, fs.FileMode(0640)); err != nil {
return "", fmt.Errorf("failed to write icon: %w", err)
}
if err := os.Rename(tmpPath, filePath); err != nil {
os.Remove(tmpPath)
return "", fmt.Errorf("failed to move icon: %w", err)
}
return filePath, nil
}
func isIconContentType(header string, data []byte) bool {
ct := strings.ToLower(strings.TrimSpace(strings.Split(header, ";")[0]))
if strings.HasPrefix(ct, "image/") {
return true
}
switch strings.Split(strings.ToLower(strings.TrimSpace(http.DetectContentType(data))), ";")[0] {
case "image/svg+xml", "text/xml", "application/xml":
return true
}
return false
}
package meteo
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"time"
)
var httpClient = &http.Client{Timeout: 10 * time.Second}
var baseURL = "https://api.open-meteo.com/v1/forecast"
type WeatherResponse struct {
CurrentUnits struct {
Temperature2m string `json:"temperature_2m"`
ApparentTemperature string `json:"apparent_temperature"`
RelativeHumidity string `json:"relative_humidity_2m"`
WindSpeed10m string `json:"wind_speed_10m"`
} `json:"current_units"`
Current struct {
Temperature2m float64 `json:"temperature_2m"`
ApparentTemperature float64 `json:"apparent_temperature"`
RelativeHumidity int `json:"relative_humidity_2m"`
WeatherCode int `json:"weather_code"`
IsDay float64 `json:"is_day"`
WindSpeed10m float64 `json:"wind_speed_10m"`
} `json:"current"`
HourlyUnits struct {
Temperature2m string `json:"temperature_2m"`
WindSpeed10m string `json:"wind_speed_10m"`
PrecipitationProbability string `json:"precipitation_probability"`
} `json:"hourly_units"`
Hourly struct {
Time []string `json:"time"`
Temperature2m []float64 `json:"temperature_2m"`
WeatherCode []int `json:"weather_code"`
IsDay []int `json:"is_day"`
WindSpeed10m []float64 `json:"wind_speed_10m"`
PrecipitationProbability []int `json:"precipitation_probability"`
} `json:"hourly"`
DailyUnits struct {
TemperatureMax string `json:"temperature_2m_max"`
TemperatureMin string `json:"temperature_2m_min"`
Sunrise string `json:"sunrise"`
Sunset string `json:"sunset"`
} `json:"daily_units"`
Daily struct {
Time []string `json:"time"`
WeatherCode []int `json:"weather_code"`
TemperatureMax []float64 `json:"temperature_2m_max"`
TemperatureMin []float64 `json:"temperature_2m_min"`
Sunrise []string `json:"sunrise"`
Sunset []string `json:"sunset"`
} `json:"daily"`
}
type Options struct {
Latitude float64
Longitude float64
TimeZone string
Units string
}
func GetWeather(options Options) (WeatherResponse, error) {
current := "temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,is_day,wind_speed_10m"
daily := "temperature_2m_max,temperature_2m_min,weather_code,sunrise,sunset"
hourly := "temperature_2m,weather_code,is_day,wind_speed_10m,precipitation_probability"
windUnit := "kmh"
if options.Units == "fahrenheit" {
windUnit = "mph"
}
params := url.Values{}
params.Set("latitude", fmt.Sprintf("%f", options.Latitude))
params.Set("longitude", fmt.Sprintf("%f", options.Longitude))
params.Set("timezone", options.TimeZone)
params.Set("temperature_unit", options.Units)
params.Set("wind_speed_unit", windUnit)
params.Set("daily", daily)
params.Set("current", current)
params.Set("hourly", hourly)
params.Set("forecast_days", "2")
resp, err := httpClient.Get(baseURL + "?" + params.Encode())
if err != nil {
return WeatherResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return WeatherResponse{}, fmt.Errorf("received non-OK HTTP status %d", resp.StatusCode)
}
var weatherData WeatherResponse
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&weatherData); err != nil {
slog.Error("failed to decode weather response", "error", err)
return WeatherResponse{}, err
}
return weatherData, nil
}
package services
import "sync"
var (
snapshotMu sync.RWMutex
snapshotByStream = map[string]func(){}
)
func RegisterSnapshot(streamID string, fn func()) {
snapshotMu.Lock()
defer snapshotMu.Unlock()
snapshotByStream[streamID] = fn
}
func PublishSnapshot(streamID string) {
snapshotMu.RLock()
fn := snapshotByStream[streamID]
snapshotMu.RUnlock()
if fn != nil {
fn()
}
}
package services
import (
"context"
"encoding/json"
"math"
"runtime"
"strconv"
"sync"
"time"
"github.com/flohoss/godash/internal/readable"
"github.com/r3labs/sse/v2"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/mem"
)
type SystemService struct {
sse *sse.Server
mu sync.RWMutex
static Static
buffer Buffer
}
type Static struct {
CPU string `json:"cpu"`
RAM string `json:"ram"`
Disk string `json:"disk"`
}
type Buffer struct {
CPU Detail `json:"cpu"`
RAM Detail `json:"ram"`
Disk Detail `json:"disk"`
}
type Detail struct {
Value string `json:"value"`
Percentage int `json:"percentage"`
}
func NewSystemService(sse *sse.Server) *SystemService {
s := SystemService{sse: sse}
sse.CreateStream("system")
RegisterSnapshot("system", s.publishSnapshot)
return &s
}
func (s *SystemService) Run(ctx context.Context) error {
s.collect(ctx)
return nil
}
func (s *SystemService) publishString(id string, v string) {
data, err := json.Marshal(v)
if err != nil {
return
}
s.sse.Publish("system", &sse.Event{Event: []byte(id), Data: append([]byte(nil), data...)})
}
func (s *SystemService) publishInt(id string, n int) {
data, err := json.Marshal(n)
if err != nil {
return
}
s.sse.Publish("system", &sse.Event{Event: []byte(id), Data: append([]byte(nil), data...)})
}
func (s *SystemService) publishSnapshot() {
s.mu.RLock()
defer s.mu.RUnlock()
s.publishInt("cpu-percentage", s.buffer.CPU.Percentage)
s.publishString("ram-value", s.buffer.RAM.Value)
s.publishInt("ram-percentage", s.buffer.RAM.Percentage)
s.publishString("disk-value", s.buffer.Disk.Value)
s.publishInt("disk-percentage", s.buffer.Disk.Percentage)
}
func (s *SystemService) GetBuffer() Buffer {
s.mu.RLock()
defer s.mu.RUnlock()
return s.buffer
}
func (s *SystemService) GetStatic() Static {
s.mu.RLock()
defer s.mu.RUnlock()
return s.static
}
func (s *SystemService) computeStatic() Static {
var st Static
st.CPU = strconv.Itoa(runtime.NumCPU()) + " threads"
p, err := disk.Partitions(false)
if err == nil {
st.Disk = strconv.Itoa(len(p)) + " partitions"
}
r, err := mem.VirtualMemory()
if err == nil && r.SwapTotal > 0 {
st.RAM = readable.ReadableSize(r.SwapTotal) + " swap"
} else {
st.RAM = "no swap"
}
return st
}
func (s *SystemService) collect(ctx context.Context) {
cpuTicker := time.NewTicker(time.Second)
defer cpuTicker.Stop()
diskTicker := time.NewTicker(10 * time.Second)
defer diskTicker.Stop()
c, err := cpu.Info()
cpuModel := ""
if err == nil && len(c) > 0 {
if c[0].ModelName != "" {
cpuModel = c[0].ModelName
} else {
cpuModel = c[0].VendorID
}
}
static := s.computeStatic()
s.mu.Lock()
s.buffer.CPU.Value = cpuModel
s.static = static
s.mu.Unlock()
cpu.Percent(time.Second, false)
var prevCPUPct, prevRAMPct, prevDiskPct int
var prevRAMVal, prevDiskVal string
pollDisk := func() {
diskStat, err := disk.Usage("/")
if err != nil {
return
}
newDiskPct := int(math.Floor(diskStat.UsedPercent))
newDiskVal := readable.ReadableSizePair(diskStat.Used, diskStat.Total)
var publishes []func()
s.mu.Lock()
if newDiskVal != prevDiskVal {
prevDiskVal = newDiskVal
s.buffer.Disk.Value = newDiskVal
val := newDiskVal
publishes = append(publishes, func() { s.publishString("disk-value", val) })
}
if newDiskPct != prevDiskPct {
prevDiskPct = newDiskPct
s.buffer.Disk.Percentage = newDiskPct
pct := newDiskPct
publishes = append(publishes, func() { s.publishInt("disk-percentage", pct) })
}
s.mu.Unlock()
for _, fn := range publishes {
fn()
}
}
pollDisk()
for {
select {
case <-cpuTicker.C:
cpuPercent, err := cpu.Percent(0, false)
if err != nil || len(cpuPercent) == 0 {
continue
}
memStat, err := mem.VirtualMemory()
if err != nil {
continue
}
newCPUPct := int(math.Floor(cpuPercent[0]))
newRAMPct := int(math.Floor(memStat.UsedPercent))
newRAMVal := readable.ReadableSizePair(memStat.Used, memStat.Total)
var publishes []func()
s.mu.Lock()
if newCPUPct != prevCPUPct {
prevCPUPct = newCPUPct
s.buffer.CPU.Percentage = newCPUPct
pct := newCPUPct
publishes = append(publishes, func() { s.publishInt("cpu-percentage", pct) })
}
if newRAMVal != prevRAMVal {
prevRAMVal = newRAMVal
s.buffer.RAM.Value = newRAMVal
val := newRAMVal
publishes = append(publishes, func() { s.publishString("ram-value", val) })
}
if newRAMPct != prevRAMPct {
prevRAMPct = newRAMPct
s.buffer.RAM.Percentage = newRAMPct
pct := newRAMPct
publishes = append(publishes, func() { s.publishInt("ram-percentage", pct) })
}
s.mu.Unlock()
for _, fn := range publishes {
fn()
}
case <-diskTicker.C:
pollDisk()
case <-ctx.Done():
return
}
}
}
package services
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"slices"
"sync"
"time"
"github.com/flohoss/godash/config"
"github.com/flohoss/godash/pkg/meteo"
"github.com/r3labs/sse/v2"
)
var fetchWeather = meteo.GetWeather
type WeatherService struct {
weather []Day
hourly []Hour
sse *sse.Server
mu sync.RWMutex
lastResponse *meteo.WeatherResponse
loc *time.Location
}
type Day struct {
Name string `json:"name"`
TemperatureMax string `json:"temperature_max"`
TemperatureMin string `json:"temperature_min"`
Icon string `json:"icon"`
More More `json:"more"`
}
type Hour struct {
Time string `json:"time"`
Temperature string `json:"temperature"`
Icon string `json:"icon"`
WindSpeed string `json:"wind_speed"`
PrecipProb string `json:"precip_prob"`
}
type More struct {
CurrentTemperature string `json:"current_temperature"`
ApparentTemperature string `json:"apparent_temperature"`
Humidity string `json:"humidity"`
WindSpeed string `json:"wind_speed"`
Sunrise string `json:"sunrise"`
Sunset string `json:"sunset"`
}
func NewWeatherService(sse *sse.Server) *WeatherService {
loc, err := time.LoadLocation(config.GetTimeZone())
if err != nil {
loc = time.Local
}
w := &WeatherService{sse: sse, loc: loc}
sse.CreateStream("weather")
RegisterSnapshot("weather", w.publishSnapshot)
w.weather = []Day{{
Name: "Loading...",
TemperatureMax: "--",
TemperatureMin: "--",
Icon: "icon-[bi--cloud-fill]",
More: More{},
}}
return w
}
func (w *WeatherService) Run(ctx context.Context) error {
if err := w.fetchAndPublish(); err != nil {
slog.Error("Failed initial weather fetch", "error", err)
}
w.collect(ctx)
return nil
}
func (w *WeatherService) publishSnapshot() {
w.mu.RLock()
weather := w.weather
hourly := w.hourly
w.mu.RUnlock()
if len(weather) == 0 {
return
}
w.publishCurrent(weather[0])
w.publishHourly(hourly)
}
func (w *WeatherService) publishCurrent(day Day) {
data, err := json.Marshal(day)
if err != nil {
return
}
w.sse.Publish("weather", &sse.Event{Event: []byte("current"), Data: append([]byte(nil), data...)})
}
func (w *WeatherService) publishHourly(hours []Hour) {
data, err := json.Marshal(hours)
if err != nil {
return
}
w.sse.Publish("weather", &sse.Event{Event: []byte("hourly"), Data: append([]byte(nil), data...)})
}
func (w *WeatherService) GetCurrentWeather() []Day {
w.mu.RLock()
defer w.mu.RUnlock()
return w.weather
}
func (w *WeatherService) GetCurrentHourly() []Hour {
w.mu.RLock()
defer w.mu.RUnlock()
return w.hourly
}
func (w *WeatherService) collect(ctx context.Context) {
fetchTicker := time.NewTicker(5 * time.Minute)
defer fetchTicker.Stop()
hourlyTicker := time.NewTicker(1 * time.Minute)
defer hourlyTicker.Stop()
for {
select {
case <-fetchTicker.C:
if err := w.fetchAndPublish(); err != nil {
slog.Error("Failed to update weather", "error", err)
}
case <-hourlyTicker.C:
w.recomputeHourly()
case <-ctx.Done():
return
}
}
}
func (w *WeatherService) recomputeHourly() {
w.mu.RLock()
res := w.lastResponse
prev := w.hourly
loc := w.loc
w.mu.RUnlock()
if res == nil {
return
}
hours := buildHourly(res, loc)
if slices.Equal(prev, hours) {
return
}
w.mu.Lock()
w.hourly = hours
w.mu.Unlock()
w.publishHourly(hours)
}
func (w *WeatherService) fetchAndPublish() error {
settings, tz := config.GetWeatherConfig()
res, err := fetchWeather(meteo.Options{
Latitude: settings.Latitude,
Longitude: settings.Longitude,
TimeZone: tz,
Units: settings.Units,
})
if err != nil {
return err
}
if len(res.Daily.Time) == 0 || len(res.Daily.Sunrise) == 0 || len(res.Daily.Sunset) == 0 {
return fmt.Errorf("incomplete weather data received")
}
loc, err := time.LoadLocation(tz)
if err != nil {
loc = time.Local
}
w.mu.RLock()
hasChanged := w.lastResponse == nil || w.hasResponseChanged(&res)
w.mu.RUnlock()
if !hasChanged {
w.mu.Lock()
w.lastResponse = &res
w.loc = loc
w.mu.Unlock()
w.recomputeHourly()
return nil
}
newWeather := []Day{}
for i, t := range res.Daily.Time {
t, _ := time.Parse("2006-01-02", t)
day := Day{
Name: t.Format("Mon 02 Jan"),
TemperatureMax: fmt.Sprintf("%.1f %s", res.Daily.TemperatureMax[i], res.DailyUnits.TemperatureMax),
TemperatureMin: fmt.Sprintf("%.1f %s", res.Daily.TemperatureMin[i], res.DailyUnits.TemperatureMin),
Icon: getIcon(res.Daily.WeatherCode[i], res.Current.IsDay != 0),
}
if i == 0 {
sunrise, _ := time.Parse("2006-01-02T15:04", res.Daily.Sunrise[0])
sunset, _ := time.Parse("2006-01-02T15:04", res.Daily.Sunset[0])
day.Icon = getIcon(res.Current.WeatherCode, res.Current.IsDay != 0)
day.More = More{
CurrentTemperature: fmt.Sprintf("%.1f %s", res.Current.Temperature2m, res.CurrentUnits.Temperature2m),
ApparentTemperature: fmt.Sprintf("%.1f %s", res.Current.ApparentTemperature, res.CurrentUnits.ApparentTemperature),
Humidity: fmt.Sprintf("%d %s", res.Current.RelativeHumidity, res.CurrentUnits.RelativeHumidity),
WindSpeed: fmt.Sprintf("%.0f %s", res.Current.WindSpeed10m, res.CurrentUnits.WindSpeed10m),
Sunrise: sunrise.Format("15:04"),
Sunset: sunset.Format("15:04"),
}
}
newWeather = append(newWeather, day)
}
hours := buildHourly(&res, loc)
w.mu.Lock()
w.weather = newWeather
w.hourly = hours
w.lastResponse = &res
w.loc = loc
w.mu.Unlock()
w.publishCurrent(newWeather[0])
w.publishHourly(hours)
return nil
}
func buildHourly(res *meteo.WeatherResponse, loc *time.Location) []Hour {
if len(res.Hourly.Time) == 0 {
return []Hour{}
}
parsed := make([]time.Time, len(res.Hourly.Time))
for i, ts := range res.Hourly.Time {
t, err := time.ParseInLocation("2006-01-02T15:04", ts, loc)
if err != nil {
return []Hour{}
}
parsed[i] = t
}
now := time.Now().In(loc)
start := now.Truncate(time.Hour).Add(time.Hour)
hours := make([]Hour, 0, 8)
maxIter := len(parsed) + 2
for t := start; len(hours) < 8 && maxIter > 0; t = t.Add(time.Hour) {
maxIter--
idx := nearestHourIndex(parsed, t)
if idx < 0 {
continue
}
isDay := res.Hourly.IsDay[idx] != 0
hours = append(hours, Hour{
Time: t.Format("15:04"),
Temperature: fmt.Sprintf("%.0f %s", res.Hourly.Temperature2m[idx], res.HourlyUnits.Temperature2m),
Icon: getIcon(res.Hourly.WeatherCode[idx], isDay),
WindSpeed: fmt.Sprintf("%.0f %s", res.Hourly.WindSpeed10m[idx], res.HourlyUnits.WindSpeed10m),
PrecipProb: fmt.Sprintf("%d%%", res.Hourly.PrecipitationProbability[idx]),
})
}
return hours
}
func nearestHourIndex(parsed []time.Time, target time.Time) int {
if len(parsed) == 0 {
return -1
}
idx, found := slices.BinarySearchFunc(parsed, target, func(t, target time.Time) int {
return t.Compare(target)
})
if found {
return idx
}
if idx == 0 {
return 0
}
if idx >= len(parsed) {
return len(parsed) - 1
}
if parsed[idx].Sub(target).Abs() >= parsed[idx-1].Sub(target).Abs() {
return idx - 1
}
return idx
}
func (w *WeatherService) hasResponseChanged(newRes *meteo.WeatherResponse) bool {
if w.lastResponse == nil {
return true
}
prev := w.lastResponse
if prev.Current.Temperature2m != newRes.Current.Temperature2m ||
prev.Current.WeatherCode != newRes.Current.WeatherCode ||
prev.Current.IsDay != newRes.Current.IsDay ||
prev.Current.RelativeHumidity != newRes.Current.RelativeHumidity ||
prev.Current.ApparentTemperature != newRes.Current.ApparentTemperature ||
prev.Current.WindSpeed10m != newRes.Current.WindSpeed10m {
return true
}
if len(prev.Daily.TemperatureMax) > 0 && len(newRes.Daily.TemperatureMax) > 0 {
if prev.Daily.TemperatureMax[0] != newRes.Daily.TemperatureMax[0] ||
prev.Daily.TemperatureMin[0] != newRes.Daily.TemperatureMin[0] {
return true
}
}
if len(prev.Daily.TemperatureMax) > 1 && len(newRes.Daily.TemperatureMax) > 1 {
if prev.Daily.TemperatureMax[1] != newRes.Daily.TemperatureMax[1] ||
prev.Daily.TemperatureMin[1] != newRes.Daily.TemperatureMin[1] ||
prev.Daily.WeatherCode[1] != newRes.Daily.WeatherCode[1] {
return true
}
}
return false
}
func getIcon(code int, isDay bool) string {
switch code {
case 0:
if isDay {
return "icon-[bi--sun-fill]"
}
return "icon-[bi--moon-fill]"
case 1, 2:
if isDay {
return "icon-[bi--cloud-sun-fill]"
}
return "icon-[bi--cloud-moon-fill]"
case 3:
return "icon-[bi--cloud-fill]"
case 45, 48:
return "icon-[bi--cloud-fog2-fill]"
case 51, 53, 55, 56, 57, 61, 66, 67, 80:
return "icon-[bi--cloud-drizzle-fill]"
case 63, 65, 81:
return "icon-[bi--cloud-rain-heavy-fill]"
case 71, 73, 75, 77, 85, 86:
return "icon-[bi--cloud-snow-fill]"
case 82, 95, 96, 99:
return "icon-[bi--cloud-lightning-rain-fill]"
default:
return "icon-[bi--cloud-fill]"
}
}
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package bookmarks
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/flohoss/godash/config"
"strings"
)
func Bookmarks(bookmarks []config.Category) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, a := range bookmarks {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"grid gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if a.Category != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"heading\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(a.Category)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 13, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"grid-apps\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, entry := range a.Entries {
templ_7745c5c3_Err = bookmark(entry).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func placeHolder(app config.App) string {
if app.Name == "" {
return "?"
}
return strings.ToUpper(app.Name[:1])
}
func noIcon(app config.App) bool {
return app.Icon == ""
}
func displayDark(app config.App) bool {
return !app.IgnoreDark && app.IconLight != ""
}
func bgIcon(path, class string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var4 = []any{"size-8 bg-contain bg-center", class}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url(" + path + ")")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 43, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func bookmark(application config.App) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(application.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 48, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"hover-effect flex items-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if displayDark(application) {
templ_7745c5c3_Err = bgIcon(application.Icon, "dark:hidden").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bgIcon(application.IconLight, "hidden dark:block").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if noIcon(application) {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"avatar avatar-placeholder\"><div class=\"bg-primary text-primary-content w-8 rounded-full\"><span class=\"text-xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(placeHolder(application))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 55, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = bgIcon(application.Icon, "").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"uppercase truncate ml-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(application.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/bookmarks/bookmarks.templ`, Line: 61, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"os"
"time"
"github.com/flohoss/godash/config"
"github.com/flohoss/godash/services"
"github.com/flohoss/godash/views/bookmarks"
"github.com/flohoss/godash/views/links"
"github.com/flohoss/godash/views/system"
"github.com/flohoss/godash/views/weather"
)
func appendVersionQuery(url string) string {
v := os.Getenv("APP_VERSION")
if v == "" {
v = time.Now().Format("20060102150405")
}
return url + "?v=" + v
}
func Home(title string, b []config.Category, l []config.Category, buffer services.Buffer, static services.Static, d []services.Day, h []services.Hour) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/home.templ`, Line: 28, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"A blazing fast start-page for services written in Go \"><meta name=\"theme-color\" content=\"#d07915\"><link rel=\"icon\" type=\"image/x-icon\" href=\"/assets/favicon/favicon.ico\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/favicon/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/assets/favicon/favicon-16x16.png\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/assets/favicon/apple-touch-icon.png\"><link rel=\"manifest\" href=\"/assets/favicon/site.webmanifest\"><link rel=\"stylesheet\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(appendVersionQuery("/assets/css/style.css"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/home.templ`, Line: 37, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><script src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(appendVersionQuery("/assets/js/sse.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/home.templ`, Line: 38, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" defer></script></head><body><main id=\"main\" class=\"container\"><div class=\"mt-4 mb-8 md:my-8 lg:my-10\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = weather.Weather(d, h).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"py-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = system.System(buffer, static).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"my-4 md:my-8 lg:my-10\"><div class=\"grid gap-8 lg:gap-12\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bookmarks.Bookmarks(b).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = links.Links(l).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div></main></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package links
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/flohoss/godash/config"
func link(link config.App) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(link.URL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/links/links.templ`, Line: 6, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"hover-effect\"><div class=\"truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/links/links.templ`, Line: 7, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func Links(links []config.Category) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"grid-apps\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, l := range links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if l.Category != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"heading\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(l.Category)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/links/links.templ`, Line: 16, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"my-[0.9rem]\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, entry := range l.Entries {
templ_7745c5c3_Err = link(entry).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package system
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"github.com/flohoss/godash/services"
)
func Badge(id, icon, static string, details services.Detail) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
hideSmall := id == "disk"
var templ_7745c5c3_Var2 = []any{"grid gap-1 w-full", templ.KV("hidden md:grid", hideSmall)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><div class=\"flex gap-2 items-start justify-between w-full min-w-0\"><div><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue("value-" + id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 16, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"truncate flex-1 overflow-hidden whitespace-nowrap\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(details.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 18, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(details.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 20, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"text-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(static)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 23, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 = []any{"size-6 shrink-0 mt-1", icon}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"></div></div><div class=\"w-full h-1 md:h-[0.4rem] overflow-hidden rounded-full bg-base-content/5\"><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("bar-" + id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 30, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"h-1 md:h-[0.4rem] transition-[width] duration-700 ease-in-out bg-primary/90\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", details.Percentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/system/system.templ`, Line: 32, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func System(buffer services.Buffer, static services.Static) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-8\" data-sse=\"/sse?stream=system\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Badge("cpu", "icon-[bi--cpu]", static.CPU, buffer.CPU).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Badge("ram", "icon-[bi--memory]", static.RAM, buffer.RAM).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Badge("disk", "icon-[bi--hdd]", static.Disk, buffer.Disk).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package weather
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"github.com/flohoss/godash/services"
)
func Weather(d []services.Day, h []services.Hour) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div data-sse=\"/sse?stream=weather\"><div class=\"flex justify-between items-center gap-4 lg:gap-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Current(d[0]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Hourly(h).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func Current(today services.Day) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex flex-row items-center gap-4 md:gap-6 lg:gap-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{"icon size-26 md:size-28 lg:size-30 shrink-0", today.Icon}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"weather-icon-0\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></div><div class=\"flex flex-1 flex-col gap-2 min-w-0\"><div class=\"flex flex-col\"><div id=\"day-name-0\" class=\"text-secondary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(today.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 25, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"flex flex-wrap items-baseline gap-x-3 gap-y-1 whitespace-nowrap\"><div id=\"temp\" class=\"font-semibold text-4xl md:text-5xl tracking-tight\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(today.More.CurrentTemperature)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 27, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div></div><div class=\"flex flex-col gap-0.5\"><div class=\"flex items-center gap-3 text-secondary text-xs\"><span class=\"flex items-center gap-1\"><span class=\"icon-[carbon--temperature-max] size-4\"></span><div id=\"max-temp-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(today.TemperatureMax)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 34, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></span> <span class=\"flex items-center gap-1\"><span class=\"icon-[carbon--temperature-min] size-4\"></span><div id=\"min-temp-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(today.TemperatureMin)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 38, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></span></div><div class=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-secondary text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = stat("icon-[carbon--temperature-feels-like]", "apparent", today.More.ApparentTemperature).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = stat("icon-[carbon--humidity]", "humidity", today.More.Humidity).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = stat("icon-[carbon--windy]", "wind", today.More.WindSpeed).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = stat("icon-[carbon--sunrise]", "sunrise", today.More.Sunrise).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = stat("icon-[carbon--sunset]", "sunset", today.More.Sunset).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func stat(iconClass string, id string, value string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex items-center gap-1 whitespace-nowrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 = []any{"shrink-0 size-4 opacity-60", iconClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></span><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 56, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 56, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func Hourly(hours []services.Hour) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
if templ_7745c5c3_Var14 == nil {
templ_7745c5c3_Var14 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div id=\"hourly\" class=\"hidden md:flex flex-1 gap-4 md:gap-6 pb-1 opacity-95 justify-end\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i := range hours {
templ_7745c5c3_Err = hourCard(i, hours[i]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func hourCard(i int, h services.Hour) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var15 := templ.GetChildren(ctx)
if templ_7745c5c3_Var15 == nil {
templ_7745c5c3_Var15 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
responsive := ""
if i >= 6 {
responsive = "hidden 2xl:flex"
} else if i >= 4 {
responsive = "hidden xl:flex"
} else if i >= 2 {
responsive = "hidden lg:flex"
}
var templ_7745c5c3_Var16 = []any{"flex w-16 shrink-0 flex-col items-end gap-1 px-2 py-1.5", responsive}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var16).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><div class=\"flex flex-col items-end gap-0.5\"><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("hour-time-%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 84, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-secondary text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(h.Time)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 84, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 = []any{"icon size-8 shrink-0", h.Icon}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("hour-icon-%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 85, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"></div><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("hour-temp-%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 86, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"font-semibold text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(h.Temperature)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 86, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div><div class=\"flex flex-col items-end gap-0.5 text-secondary text-xs w-full\"><div class=\"flex items-center justify-center gap-1 whitespace-nowrap\"><span class=\"icon-[carbon--windy] size-3.5 shrink-0\"></span><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("hour-wind-%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 91, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"text-secondary whitespace-nowrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(h.WindSpeed)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 91, Col: 101}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div></div><div class=\"flex items-center justify-center gap-1\"><span class=\"icon-[carbon--rain] size-3.5 shrink-0\"></span><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("hour-precip-%d", i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 95, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"text-secondary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(h.PrecipProb)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/weather/weather.templ`, Line: 95, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate