Skip to content

Commit

Permalink
feat: Initial dynamic icon implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Mar 1, 2024
1 parent 11436b4 commit 9cdfc0d
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 7 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
require (
fyne.io/systray v1.10.1-0.20240111184411-11c585fff98d
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346
github.com/knadh/koanf/providers/file v0.1.0
github.com/knadh/koanf/providers/rawbytes v0.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsM
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346 h1:Odeq5rB6OZSkib5gqTG+EM1iF0bUVjYYd33XB1ULv00=
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346/go.mod h1:4ggHM2qnyyZjenBb7RpwVzIj+JMsu9kHCVxMjB30hGs=
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
Expand Down
23 changes: 16 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import (
)

type Config struct {
Title string `toml:"title" comment:"Tray title."`
URL string `toml:"url" comment:"Nightscout URL. (required)"`
Token string `toml:"token" comment:"Nightscout token. Using an access token is recommended instead of the API secret."`
Units string `toml:"units" comment:"Blood sugar unit. (one of: mg/dL, mmol/L)"`
Interval Duration `toml:"interval" comment:"Update interval."`
Arrows Arrows `toml:"arrows" comment:"Customize the arrows."`
LocalFile LocalFile `toml:"local-file" comment:"Enables writing the latest blood sugar to a local temporary file."`
Title string `toml:"title" comment:"Tray title."`
URL string `toml:"url" comment:"Nightscout URL. (required)"`
Token string `toml:"token" comment:"Nightscout token. Using an access token is recommended instead of the API secret."`
Units string `toml:"units" comment:"Blood sugar unit. (one of: mg/dL, mmol/L)"`
DynamicIcon DynamicIcon `toml:"dynamic_icon" comment:"Makes the tray icon show the current blood sugar reading."`
Interval Duration `toml:"interval" comment:"Update interval."`
Arrows Arrows `toml:"arrows" comment:"Customize the arrows."`
LocalFile LocalFile `toml:"local-file" comment:"Enables writing the latest blood sugar to a local temporary file."`
}

type DynamicIcon struct {
Enabled bool `toml:"enabled"`
FontColor string `toml:"font_color" comment:"(one of: white, black)"`
FontFile string `toml:"font_file" comment:"If left blank, an embedded font will be used."`
FontSize float64 `toml:"font_size" comment:"Font size in points."`
YOffset int `toml:"y_offset" comment:"Vertical offset."`
}

type Arrows struct {
Expand Down
16 changes: 16 additions & 0 deletions internal/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"path/filepath"
"runtime"
"time"
)

Expand All @@ -10,10 +11,25 @@ var Default = NewDefault()
const LocalFileFormatCsv = "csv"

func NewDefault() Config {
dynamicIconEnabled := true
dynamicIconColor := "white"
switch runtime.GOOS {
case "darwin":
dynamicIconEnabled = false
case "windows":
dynamicIconColor = "black"
}

return Config{
Title: "Nightscout",
Units: UnitsMgdl,
Interval: Duration{30 * time.Second},
DynamicIcon: DynamicIcon{
Enabled: dynamicIconEnabled,
FontColor: dynamicIconColor,
FontSize: 18.5,
YOffset: 4,
},
Arrows: Arrows{
DoubleUp: "⇈",
SingleUp: "↑",
Expand Down
Binary file added internal/dynamic_icon/Roboto-Bold.ttf
Binary file not shown.
70 changes: 70 additions & 0 deletions internal/dynamic_icon/dynamic_icon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dynamic_icon

import (
"bytes"
_ "embed"
"errors"
"fmt"
"image"
"os"

"github.com/gabe565/nightscout-menu-bar/internal/config"
"github.com/gabe565/nightscout-menu-bar/internal/nightscout"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
)

var (
//go:embed Roboto-Bold.ttf
embeddedFont []byte

face *truetype.Font

ErrInvalidColor = errors.New("invalid color")
)

func Generate(p *nightscout.Properties) ([]byte, error) {
if face == nil {
var b []byte
if config.Default.DynamicIcon.FontFile == "" {
b = embeddedFont
} else {
var err error
if b, err = os.ReadFile(config.Default.DynamicIcon.FontFile); err != nil {
return nil, err
}
}

var err error
if face, err = freetype.ParseFont(b); err != nil {
return nil, err
}
}

img := image.NewRGBA(image.Rectangle{Max: image.Point{X: 32, Y: 32}})
c := freetype.NewContext()
c.SetFont(face)
c.SetFontSize(config.Default.DynamicIcon.FontSize)
c.SetClip(img.Bounds())
c.SetDst(img)
switch config.Default.DynamicIcon.FontColor {
case "white":
c.SetSrc(image.White)
case "black":
c.SetSrc(image.Black)
default:
return nil, fmt.Errorf("%w: %s", ErrInvalidColor, config.Default.DynamicIcon.FontColor)
}

pt := freetype.Pt(0, config.Default.DynamicIcon.YOffset+int(c.PointToFixed(config.Default.DynamicIcon.FontSize)>>6))
if _, err := c.DrawString(p.Bgnow.DisplayBg(), pt); err != nil {
return nil, err
}

var buf bytes.Buffer
if err := encode(&buf, img); err != nil {
return nil, err
}

return buf.Bytes(), nil
}
13 changes: 13 additions & 0 deletions internal/dynamic_icon/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !windows

package dynamic_icon

import (
"image"
"image/png"
"io"
)

func encode(w io.Writer, img image.Image) error {
return png.Encode(w, img)
}
69 changes: 69 additions & 0 deletions internal/dynamic_icon/encode_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package dynamic_icon

import (
"bytes"
"encoding/binary"
"image"
"image/png"
"io"
)

type iconDir struct {
reserved uint16
imageType uint16
numImages uint16
}

type iconDirEntry struct {
imageWidth uint8
imageHeight uint8
numColors uint8
reserved uint8
colorPlanes uint16
bitsPerPixel uint16
sizeInBytes uint32
offset uint32
}

func newIcondir() iconDir {
return iconDir{
imageType: 1,
numImages: 1,
}
}

func newIcondirentry() iconDirEntry {
return iconDirEntry{
colorPlanes: 1, // windows is supposed to not mind 0 or 1, but other icon files seem to have 1 here
bitsPerPixel: 32, // can be 24 for bitmap or 24/32 for png. Set to 32 for now
offset: 22, // 6 iconDir + 16 iconDirEntry, next image will be this image size + 16 iconDirEntry, etc
}
}

func encode(w io.Writer, img image.Image) error {
dir := newIcondir()
entry := newIcondirentry()

var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return err
}
entry.sizeInBytes = uint32(buf.Len())

bounds := img.Bounds()
entry.imageWidth = uint8(bounds.Dx())
entry.imageHeight = uint8(bounds.Dy())

if err := binary.Write(w, binary.LittleEndian, dir); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, entry); err != nil {
return err
}

if _, err := buf.WriteTo(w); err != nil {
return err
}

return nil
}
9 changes: 9 additions & 0 deletions internal/tray/systray.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gabe565/nightscout-menu-bar/internal/assets"
"github.com/gabe565/nightscout-menu-bar/internal/autostart"
"github.com/gabe565/nightscout-menu-bar/internal/config"
"github.com/gabe565/nightscout-menu-bar/internal/dynamic_icon"
"github.com/gabe565/nightscout-menu-bar/internal/local_file"
"github.com/gabe565/nightscout-menu-bar/internal/nightscout"
"github.com/gabe565/nightscout-menu-bar/internal/tray/items"
Expand Down Expand Up @@ -113,6 +114,14 @@ func onReady() {
historyVals = append(historyVals, entry)
}
}

if config.Default.DynamicIcon.Enabled {
if icon, err := dynamic_icon.Generate(properties); err == nil {
systray.SetTemplateIcon(icon, icon)
} else {
slog.Error("Failed to generate icon", "error", err.Error())
}
}
case err := <-Error:
errorItem.SetTitle(err.Error())
errorItem.Show()
Expand Down

0 comments on commit 9cdfc0d

Please sign in to comment.