Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timer widget #67

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,40 @@ corresponding icons with correct names need to be placed in
`~/.local/share/deckmaster/themes/[theme]`. The default icons with their
respective names can be found [here](https://github.com/muesli/deckmaster/tree/master/assets/weather).

#### Timer

A flexible widget that can display a timer/countdown and displays its remaining time.

```toml
[keys.widget]
id = "timer"
[keys.widget.config]
times = "5s;10m;30m;1h5m" # optional
font = "bold;regular;thin" # optional
color = "#fefefe;#0f0f0f;#00ff00;" # optional
underflow = "false" # optional
underflowColor = "#ff0000;#ff0000;#ff0000" # optional
```

With `layout` custom layouts can be definded in the format `[posX]x[posY]+[width]x[height]`.

Values for `format` are:

| % | gets replaced with |
| --- | ------------------------------------------------------------------ |
| %h | 12-hour format of an hour with leading zeros |
| %H | 24-hour format of an hour with leading zeros |
| %i | Minutes with leading zeros |
| %I | Minutes without leading zeros |
| %s | Seconds with leading zeros |
| %S | Seconds without leading zeros |
| %a | Lowercase Ante meridiem and Post meridiem |

The timer can be started and paused by short pressing the button.
When triggering the hold action the next timer in the times list is selected if
no timer is running. If the timer is paused, it will be reset.
The setting underflow determines whether the timer keeps ticking after exceeding its deadline.

### Background Image

You can configure each deck to display an individual wallpaper behind its
Expand Down
22 changes: 22 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"reflect"
"strconv"
"strings"
"time"

"github.com/BurntSushi/toml"
colorful "github.com/lucasb-eyer/go-colorful"
Expand Down Expand Up @@ -136,6 +137,14 @@ func ConfigValue(v interface{}, dst interface{}) error {
default:
return fmt.Errorf("unhandled type %+v for color.Color conversion", reflect.TypeOf(vt))
}
case *time.Duration:
switch vt := v.(type) {
case string:
x, _ := time.ParseDuration(vt)
*d = x
default:
return fmt.Errorf("unhandled type %+v for time.Duration conversion", reflect.TypeOf(vt))
}

case *[]string:
switch vt := v.(type) {
Expand All @@ -158,6 +167,19 @@ func ConfigValue(v interface{}, dst interface{}) error {
default:
return fmt.Errorf("unhandled type %+v for []color.Color conversion", reflect.TypeOf(vt))
}
case *[]time.Duration:
switch vt := v.(type) {
case string:
durationsString := strings.Split(vt, ";")
var durations []time.Duration
for _, durationString := range durationsString {
duration, _ := time.ParseDuration(durationString)
durations = append(durations, duration)
}
*d = durations
default:
return fmt.Errorf("unhandled type %+v for []time.Duration conversion", reflect.TypeOf(vt))
}

default:
return fmt.Errorf("unhandled dst type %+v", reflect.TypeOf(dst))
Expand Down
13 changes: 13 additions & 0 deletions decks/main.deck
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@
[keys.action_hold]
keycode = "Mute"

[[keys]]
index = 7
[keys.widget]
id = "timer"
[keys.widget.config]
times = "5s;10m;30m;1h5m" # optional
format = "%Hh;%Im;%Ss"
font = "bold;regular;thin" # optional
#color = "#fefefe" # optional
underflow = "false" # optional
underflowColor = "#ff0000;#ff0000;#ff0000" # optional


[[keys]]
index = 8
[keys.widget]
Expand Down
3 changes: 3 additions & 0 deletions widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func NewWidget(dev *streamdeck.Device, base string, kc KeyConfig, bg image.Image

case "weather":
return NewWeatherWidget(bw, kc.Widget)

case "timer":
return NewTimerWidget(bw, kc.Widget), nil
}

// unknown widget ID
Expand Down
186 changes: 186 additions & 0 deletions widget_timer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package main

import (
"image"
"image/color"
"strings"
"time"
)

// TimerWidget is a widget displaying a timer
type TimerWidget struct {
*BaseWidget

times []time.Duration

formats []string
fonts []string
colors []color.Color
frames []image.Rectangle

underflow bool
underflowColors []color.Color
currIndex int

data TimerData
}

type TimerData struct {
startTime time.Time
pausedTime time.Time
}

func (d *TimerData) IsPaused() bool {
return !d.pausedTime.IsZero()
}

func (d *TimerData) IsRunning() bool {
return !d.IsPaused() && d.HasDeadline()
}

func (d *TimerData) HasDeadline() bool {
return !d.startTime.IsZero()
}

func (d *TimerData) Clear() {
d.startTime = time.Time{}
d.pausedTime = time.Time{}
}

// NewTimerWidget returns a new TimerWidget
func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget {
bw.setInterval(time.Duration(opts.Interval)*time.Millisecond, time.Second/2)

var times []time.Duration
var formats, fonts, frameReps []string
var colors, underflowColors []color.Color
var underflow bool

_ = ConfigValue(opts.Config["times"], &times)

_ = ConfigValue(opts.Config["format"], &formats)
_ = ConfigValue(opts.Config["font"], &fonts)
_ = ConfigValue(opts.Config["color"], &colors)
_ = ConfigValue(opts.Config["layout"], &frameReps)

_ = ConfigValue(opts.Config["underflow"], &underflow)
_ = ConfigValue(opts.Config["underflowColor"], &underflowColors)

if len(times) == 0 {
defaultDuration, _ := time.ParseDuration("30m")
times = append(times, defaultDuration)
}

layout := NewLayout(int(bw.dev.Pixels))
frames := layout.FormatLayout(frameReps, len(formats))

for i := 0; i < len(formats); i++ {
if len(fonts) < i+1 {
fonts = append(fonts, "regular")
}
if len(colors) < i+1 {
colors = append(colors, DefaultColor)
}
if len(underflowColors) < i+1 {
underflowColors = append(underflowColors, DefaultColor)
}
}

return &TimerWidget{
BaseWidget: bw,
times: times,
formats: formats,
fonts: fonts,
colors: colors,
frames: frames,
underflow: underflow,
underflowColors: underflowColors,
currIndex: 0,
data: TimerData{
startTime: time.Time{},
pausedTime: time.Time{},
},
}
}

// Update renders the widget.
func (w *TimerWidget) Update() error {
if w.data.IsPaused() {
return nil
}
size := int(w.dev.Pixels)
img := image.NewRGBA(image.Rect(0, 0, size, size))
var str string

for i := 0; i < len(w.formats); i++ {
var fontColor = w.colors[i]

if !w.data.HasDeadline() {
str = Timespan(w.times[w.currIndex]).Format(w.formats[i])
} else {
remainingDuration := time.Until(w.data.startTime.Add(w.times[w.currIndex]))
if remainingDuration < 0 && !w.underflow {
str = Timespan(w.times[w.currIndex]).Format(w.formats[i])
w.data.Clear()
} else if remainingDuration < 0 && w.underflow {
fontColor = w.underflowColors[i]
str = Timespan(remainingDuration * -1).Format(w.formats[i])
} else {
str = Timespan(remainingDuration).Format(w.formats[i])
}
}
Comment on lines +118 to +131
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be a method(s) .. not really happy about how this turned out. Its a bit hard to read.

ie line 123, this is basically rendering the current timer you can start. Ideally we should be telling the types what to do .. now we are kinda asking them for information on how to generate a string to display. Abit of data jealousy.

font := fontByName(w.fonts[i])

drawString(img,
w.frames[i],
font,
str,
w.dev.DPI,
-1,
fontColor,
image.Pt(-1, -1))
}

return w.render(w.dev, img)
}

type Timespan time.Duration

func (t Timespan) Format(format string) string {
tm := map[string]string{
"%h": "03",
"%H": "15",
"%i": "04",
"%s": "05",
"%I": "4",
"%S": "5",
"%a": "PM",
}

for k, v := range tm {
format = strings.ReplaceAll(format, k, v)
}

z := time.Unix(0, 0).UTC()
return z.Add(time.Duration(t)).Format(format)
}

func (w *TimerWidget) TriggerAction(hold bool) {
if hold {
if w.data.IsPaused() {
w.data.Clear()
} else if !w.data.HasDeadline() {
w.currIndex = (w.currIndex + 1) % len(w.times)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method? NextTimer() ?

}
} else {
if w.data.IsRunning() {
w.data.pausedTime = time.Now()
} else if w.data.IsPaused() && w.data.HasDeadline() {
pausedDuration := time.Now().Sub(w.data.pausedTime)
w.data.startTime = w.data.startTime.Add(pausedDuration)
w.data.pausedTime = time.Time{}
Comment on lines +179 to +181
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method? Resume() ?

} else {
w.data.startTime = time.Now()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method? StartTimer()

}
}
}