Skip to content

Commit

Permalink
Refactor to support customized layouts
Browse files Browse the repository at this point in the history
Support timer configuration by time.Duration formats such as 1h10m15s
etc.
Fixes pause and resume functionality.
  • Loading branch information
norrs committed Feb 3, 2022
1 parent 9bc55fe commit 93a88ed
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 100 deletions.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,21 +274,37 @@ respective names can be found [here](https://github.com/muesli/deckmaster/tree/m

#### Timer

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

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

The timer can be started and stopped by short pressing the button.
When triggering the hold action the next timer in the times list is selected.
The setting underflow determines whether the timer keeps running into negative values or not.
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

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
230 changes: 137 additions & 93 deletions widget_timer.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package main

import (
"fmt"
"image"
"image/color"
"math"
"regexp"
"strconv"
"strings"
"time"
)
Expand All @@ -15,128 +11,176 @@ import (
type TimerWidget struct {
*BaseWidget

times []string
font string
color color.Color
underflow bool
currIndex int
startTime time.Time
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 []string
_ = ConfigValue(opts.Config["times"], &times)
var font string
_ = ConfigValue(opts.Config["font"], &font)
var color color.Color
_ = ConfigValue(opts.Config["color"], &color)
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)

re := regexp.MustCompile(`^(\d{1,2}:){0,2}\d{1,2}$`)
for i := 0; i < len(times); i++ {
if !re.MatchString(times[i]) {
times = append(times[:i], times[i+1:]...)
}
}
if len(times) == 0 {
times = append(times, "30:00")
}
if font == "" {
font = "bold"
defaultDuration, _ := time.ParseDuration("30m")
times = append(times, defaultDuration)
}
if color == nil {
color = DefaultColor

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,
font: font,
color: color,
underflow: underflow,
currIndex: 0,
startTime: time.Time{},
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 {
split := strings.Split(w.times[w.currIndex], ":")
seconds := int64(0)
for i := 0; i < len(split); i++ {
val, _ := strconv.ParseInt(split[len(split)-(i+1)], 10, 64)
seconds += val * int64(math.Pow(60, float64(i)))
if w.data.IsPaused() {
return nil
}

str := ""
if w.startTime.IsZero() {
str = timerRep(seconds)
} else {
duration, _ := time.ParseDuration(strconv.FormatInt(seconds, 10) + "s")
remaining := time.Until(w.startTime.Add(duration))
if remaining < 0 && !w.underflow {
str = timerRep(0)
} else {
str = timerRep(int64(remaining.Seconds()))
}
}

size := int(w.dev.Pixels)
img := image.NewRGBA(image.Rect(0, 0, size, size))
font := fontByName(w.font)
drawString(img,
image.Rect(0, 0, size, size),
font,
str,
w.dev.DPI,
-1,
w.color,
image.Pt(-1, -1))
var str string

return w.render(w.dev, img)
}
for i := 0; i < len(w.formats); i++ {
var fontColor = w.colors[i]

func (w *TimerWidget) TriggerAction(hold bool) {
if w.startTime.IsZero() {
if hold {
w.currIndex = (w.currIndex + 1) % len(w.times)
if !w.data.HasDeadline() {
str = Timespan(w.times[w.currIndex]).Format(w.formats[i])
} else {
w.startTime = time.Now()
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.startTime = time.Time{}
} 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])
}
}
} else {
w.startTime = time.Time{}
font := fontByName(w.fonts[i])

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

func timerRep(seconds int64) string {
secs := Abs(seconds % 60)
mins := Abs(seconds / 60 % 60)
hrs := Abs(seconds / 60 / 60)
return w.render(w.dev, img)
}

str := ""
if seconds < 0 {
str += "-"
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",
}
if hrs != 0 {
str += fmt.Sprintf("%d", hrs) + ":" + fmt.Sprintf("%02d", mins) + ":" + fmt.Sprintf("%02d", secs)
} else {
if mins != 0 {
str += fmt.Sprintf("%d", mins) + ":" + fmt.Sprintf("%02d", secs)
} else {
str += fmt.Sprintf("%d", secs)
}

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

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

func Abs(x int64) int64 {
if x < 0 {
return -x
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)
}
} 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{}
} else {
w.data.startTime = time.Now()
}
}
return x
}

0 comments on commit 93a88ed

Please sign in to comment.