fields.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package widget
  2. import (
  3. "fmt"
  4. "html/template"
  5. "os"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "gopkg.in/yaml.v3"
  11. )
  12. var HSLColorPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`)
  13. var EnvFieldPattern = regexp.MustCompile(`(^|.)\$\{([A-Z_]+)\}`)
  14. const (
  15. HSLHueMax = 360
  16. HSLSaturationMax = 100
  17. HSLLightnessMax = 100
  18. )
  19. type HSLColorField struct {
  20. Hue uint16
  21. Saturation uint8
  22. Lightness uint8
  23. }
  24. func (c *HSLColorField) String() string {
  25. return fmt.Sprintf("hsl(%d, %d%%, %d%%)", c.Hue, c.Saturation, c.Lightness)
  26. }
  27. func (c *HSLColorField) AsCSSValue() template.CSS {
  28. return template.CSS(c.String())
  29. }
  30. func (c *HSLColorField) UnmarshalYAML(node *yaml.Node) error {
  31. var value string
  32. if err := node.Decode(&value); err != nil {
  33. return err
  34. }
  35. matches := HSLColorPattern.FindStringSubmatch(value)
  36. if len(matches) != 4 {
  37. return fmt.Errorf("invalid HSL color format: %s", value)
  38. }
  39. hue, err := strconv.ParseUint(matches[1], 10, 16)
  40. if err != nil {
  41. return err
  42. }
  43. if hue > HSLHueMax {
  44. return fmt.Errorf("HSL hue must be between 0 and %d", HSLHueMax)
  45. }
  46. saturation, err := strconv.ParseUint(matches[2], 10, 8)
  47. if err != nil {
  48. return err
  49. }
  50. if saturation > HSLSaturationMax {
  51. return fmt.Errorf("HSL saturation must be between 0 and %d", HSLSaturationMax)
  52. }
  53. lightness, err := strconv.ParseUint(matches[3], 10, 8)
  54. if err != nil {
  55. return err
  56. }
  57. if lightness > HSLLightnessMax {
  58. return fmt.Errorf("HSL lightness must be between 0 and %d", HSLLightnessMax)
  59. }
  60. c.Hue = uint16(hue)
  61. c.Saturation = uint8(saturation)
  62. c.Lightness = uint8(lightness)
  63. return nil
  64. }
  65. var DurationPattern = regexp.MustCompile(`^(\d+)(s|m|h|d)$`)
  66. type DurationField time.Duration
  67. func (d *DurationField) UnmarshalYAML(node *yaml.Node) error {
  68. var value string
  69. if err := node.Decode(&value); err != nil {
  70. return err
  71. }
  72. matches := DurationPattern.FindStringSubmatch(value)
  73. if len(matches) != 3 {
  74. return fmt.Errorf("invalid duration format: %s", value)
  75. }
  76. duration, err := strconv.Atoi(matches[1])
  77. if err != nil {
  78. return err
  79. }
  80. switch matches[2] {
  81. case "s":
  82. *d = DurationField(time.Duration(duration) * time.Second)
  83. case "m":
  84. *d = DurationField(time.Duration(duration) * time.Minute)
  85. case "h":
  86. *d = DurationField(time.Duration(duration) * time.Hour)
  87. case "d":
  88. *d = DurationField(time.Duration(duration) * 24 * time.Hour)
  89. }
  90. return nil
  91. }
  92. type OptionalEnvString string
  93. func (f *OptionalEnvString) UnmarshalYAML(node *yaml.Node) error {
  94. var value string
  95. err := node.Decode(&value)
  96. if err != nil {
  97. return err
  98. }
  99. replaced := EnvFieldPattern.ReplaceAllStringFunc(value, func(whole string) string {
  100. if err != nil {
  101. return ""
  102. }
  103. groups := EnvFieldPattern.FindStringSubmatch(whole)
  104. if len(groups) != 3 {
  105. return whole
  106. }
  107. prefix, key := groups[1], groups[2]
  108. if prefix == `\` {
  109. if len(whole) >= 2 {
  110. return whole[1:]
  111. } else {
  112. return ""
  113. }
  114. }
  115. value, found := os.LookupEnv(key)
  116. if !found {
  117. err = fmt.Errorf("environment variable %s not found", key)
  118. return ""
  119. }
  120. return prefix + value
  121. })
  122. if err != nil {
  123. return err
  124. }
  125. *f = OptionalEnvString(replaced)
  126. return nil
  127. }
  128. func (f *OptionalEnvString) String() string {
  129. return string(*f)
  130. }
  131. type CustomIcon struct {
  132. URL string
  133. IsFlatIcon bool
  134. // TODO: along with whether the icon is flat, we also need to know
  135. // whether the icon is black or white by default in order to properly
  136. // invert the color based on the theme being light or dark
  137. }
  138. func (i *CustomIcon) UnmarshalYAML(node *yaml.Node) error {
  139. var value string
  140. if err := node.Decode(&value); err != nil {
  141. return err
  142. }
  143. prefix, icon, found := strings.Cut(value, ":")
  144. if !found {
  145. i.URL = value
  146. return nil
  147. }
  148. switch prefix {
  149. case "si":
  150. i.URL = "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg"
  151. i.IsFlatIcon = true
  152. case "di":
  153. // syntax: di:<icon_name>[.svg|.png]
  154. // if the icon name is specified without extension, it is assumed to be wanting the SVG icon
  155. // otherwise, specify the extension of either .svg or .png to use either of the CDN offerings
  156. // any other extension will be interpreted as .svg
  157. basename, ext, found := strings.Cut(icon, ".")
  158. if !found {
  159. ext = "svg"
  160. basename = icon
  161. }
  162. if ext != "svg" && ext != "png" {
  163. ext = "svg"
  164. }
  165. i.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext
  166. default:
  167. i.URL = value
  168. }
  169. return nil
  170. }