From 3a0eca27f5922caa4d3cbb30c19294d3626dc39c Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Sat, 29 Jan 2022 22:40:40 +0100 Subject: [PATCH 1/7] Implement new timer widget --- README.md | 18 +++++++ widget.go | 3 ++ widget_timer.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 widget_timer.go diff --git a/README.md b/README.md index 995b800..52144ea 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,24 @@ 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 widget that implements a timer and displays its remaining time. + +```toml +[keys.widget] + id = "timer" + [keys.widget.config] + times = "5:00;10:00;30:00" # optional + font = "bold" # optional + color = "#fefefe" # optional + underflow = "false" # 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. + ### Background Image You can configure each deck to display an individual wallpaper behind its diff --git a/widget.go b/widget.go index 60764b1..a3175e5 100644 --- a/widget.go +++ b/widget.go @@ -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 diff --git a/widget_timer.go b/widget_timer.go new file mode 100644 index 0000000..593fa7b --- /dev/null +++ b/widget_timer.go @@ -0,0 +1,127 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "regexp" + "strconv" + "strings" + "time" +) + +// TimerWidget is a widget displaying a timer +type TimerWidget struct { + *BaseWidget + + times []string + font string + color color.Color + underflow bool + currIndex int + startTime 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"], ×) + var font string + _ = ConfigValue(opts.Config["font"], &font) + var color color.Color + _ = ConfigValue(opts.Config["color"], &color) + var underflow bool + _ = ConfigValue(opts.Config["underflow"], &underflow) + + re := regexp.MustCompile(`^\d{1,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" + } + if color == nil { + color = DefaultColor + } + + return &TimerWidget{ + BaseWidget: bw, + times: times, + font: font, + color: color, + underflow: underflow, + currIndex: 0, + startTime: time.Time{}, + } +} + +// Update renders the widget. +func (w *TimerWidget) Update() error { + size := int(w.dev.Pixels) + img := image.NewRGBA(image.Rect(0, 0, size, size)) + font := fontByName(w.font) + str := "" + split := strings.Split(w.times[w.currIndex], ":") + + if w.startTime.IsZero() { + // drop errors since we ensured the format of times above + mins, _ := strconv.ParseInt(split[0], 10, 64) + secs, _ := strconv.ParseInt(split[1], 10, 64) + str = timerRep(mins, secs) + } else { + duration, _ := time.ParseDuration(split[0] + "m" + split[1] + "s") + remaining := time.Until(w.startTime.Add(duration)) + if remaining < 0 && !w.underflow { + str = timerRep(0, 0) + } else { + seconds := int64(remaining.Seconds()) + str = timerRep(seconds/60, seconds%60) + } + } + + drawString(img, + image.Rect(0, 0, size, size), + font, + str, + w.dev.DPI, + -1, + w.color, + image.Pt(-1, -1)) + + return w.render(w.dev, img) +} + +func (w *TimerWidget) TriggerAction(hold bool) { + if w.startTime.IsZero() { + if hold { + w.currIndex = (w.currIndex + 1) % len(w.times) + } else { + w.startTime = time.Now() + } + } else { + w.startTime = time.Time{} + } +} + +func timerRep(minutes int64, seconds int64) string { + sec_str := fmt.Sprintf("%02d", Abs(seconds)) + min_str := fmt.Sprintf("%d", Abs(minutes)) + if minutes < 0 || seconds < 0 { + return "-" + min_str + ":" + sec_str + } + return min_str + ":" + sec_str +} + +func Abs(x int64) int64 { + if x < 0 { + return -x + } + return x +} From 9bc55fee2e6d0e575f9af40d67c3b1c37eb80f5c Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Sun, 30 Jan 2022 02:00:52 +0100 Subject: [PATCH 2/7] Add proper support for durations longer than one hour --- README.md | 2 +- widget_timer.go | 53 +++++++++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 52144ea..1fc2e52 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ A widget that implements a timer and displays its remaining time. [keys.widget] id = "timer" [keys.widget.config] - times = "5:00;10:00;30:00" # optional + times = "30;10:00;30:00;1:0:0" # optional font = "bold" # optional color = "#fefefe" # optional underflow = "false" # optional diff --git a/widget_timer.go b/widget_timer.go index 593fa7b..8c5fb4a 100644 --- a/widget_timer.go +++ b/widget_timer.go @@ -4,6 +4,7 @@ import ( "fmt" "image" "image/color" + "math" "regexp" "strconv" "strings" @@ -35,7 +36,7 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { var underflow bool _ = ConfigValue(opts.Config["underflow"], &underflow) - re := regexp.MustCompile(`^\d{1,2}:\d{1,2}$`) + 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:]...) @@ -64,28 +65,29 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { // Update renders the widget. func (w *TimerWidget) Update() error { - size := int(w.dev.Pixels) - img := image.NewRGBA(image.Rect(0, 0, size, size)) - font := fontByName(w.font) - str := "" 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))) + } + str := "" if w.startTime.IsZero() { - // drop errors since we ensured the format of times above - mins, _ := strconv.ParseInt(split[0], 10, 64) - secs, _ := strconv.ParseInt(split[1], 10, 64) - str = timerRep(mins, secs) + str = timerRep(seconds) } else { - duration, _ := time.ParseDuration(split[0] + "m" + split[1] + "s") + duration, _ := time.ParseDuration(strconv.FormatInt(seconds, 10) + "s") remaining := time.Until(w.startTime.Add(duration)) if remaining < 0 && !w.underflow { - str = timerRep(0, 0) + str = timerRep(0) } else { - seconds := int64(remaining.Seconds()) - str = timerRep(seconds/60, seconds%60) + 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, @@ -110,13 +112,26 @@ func (w *TimerWidget) TriggerAction(hold bool) { } } -func timerRep(minutes int64, seconds int64) string { - sec_str := fmt.Sprintf("%02d", Abs(seconds)) - min_str := fmt.Sprintf("%d", Abs(minutes)) - if minutes < 0 || seconds < 0 { - return "-" + min_str + ":" + sec_str +func timerRep(seconds int64) string { + secs := Abs(seconds % 60) + mins := Abs(seconds / 60 % 60) + hrs := Abs(seconds / 60 / 60) + + str := "" + if seconds < 0 { + str += "-" + } + 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) + } } - return min_str + ":" + sec_str + + return str } func Abs(x int64) int64 { From 354cc21318711593d84a52817099883f83a5b3a6 Mon Sep 17 00:00:00 2001 From: Roy Sindre Norangshol Date: Thu, 3 Feb 2022 21:11:45 +0100 Subject: [PATCH 3/7] Refactor to support customized layouts Support timer configuration by time.Duration formats such as 1h10m15s etc. Fixes pause and resume functionality. --- README.md | 30 +++++-- config.go | 22 +++++ widget_timer.go | 230 ++++++++++++++++++++++++++++-------------------- 3 files changed, 182 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 1fc2e52..98feab3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.go b/config.go index ad9d17a..694f576 100644 --- a/config.go +++ b/config.go @@ -8,6 +8,7 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/BurntSushi/toml" colorful "github.com/lucasb-eyer/go-colorful" @@ -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) { @@ -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)) diff --git a/widget_timer.go b/widget_timer.go index 8c5fb4a..bcde168 100644 --- a/widget_timer.go +++ b/widget_timer.go @@ -1,12 +1,8 @@ package main import ( - "fmt" "image" "image/color" - "math" - "regexp" - "strconv" "strings" "time" ) @@ -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"], ×) - 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"], ×) + + _ = 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.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]) + } } - } 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 } From f221bb46d760ff0f286ce509e4f8425629b2ae11 Mon Sep 17 00:00:00 2001 From: Roy Sindre Norangshol Date: Thu, 3 Feb 2022 21:12:31 +0100 Subject: [PATCH 4/7] My example.deck -drop this commit --- decks/main.deck | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/decks/main.deck b/decks/main.deck index c73c985..a7f1ef0 100644 --- a/decks/main.deck +++ b/decks/main.deck @@ -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] From ef337ae7ab8ee882fcc4ac9d25ace8218954954a Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Fri, 4 Feb 2022 23:42:16 +0100 Subject: [PATCH 5/7] Add and implement adaptive setting --- README.md | 3 ++- widget_timer.go | 57 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 98feab3..cb0a986 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ A flexible widget that can display a timer/countdown and displays its remaining times = "5s;10m;30m;1h5m" # optional font = "bold;regular;thin" # optional color = "#fefefe;#0f0f0f;#00ff00;" # optional + adaptive = "false" # optional underflow = "false" # optional underflowColor = "#ff0000;#ff0000;#ff0000" # optional ``` @@ -299,12 +300,12 @@ Values for `format` are: | %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. +The adaptive settings allows the removal of leading zeroes and delimiters that are not required for the time representation. ### Background Image diff --git a/widget_timer.go b/widget_timer.go index bcde168..2ba1cdf 100644 --- a/widget_timer.go +++ b/widget_timer.go @@ -18,6 +18,7 @@ type TimerWidget struct { colors []color.Color frames []image.Rectangle + adaptive bool underflow bool underflowColors []color.Color currIndex int @@ -54,7 +55,7 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { var times []time.Duration var formats, fonts, frameReps []string var colors, underflowColors []color.Color - var underflow bool + var adaptive, underflow bool _ = ConfigValue(opts.Config["times"], ×) @@ -63,6 +64,7 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { _ = ConfigValue(opts.Config["color"], &colors) _ = ConfigValue(opts.Config["layout"], &frameReps) + _ = ConfigValue(opts.Config["adaptive"], &adaptive) _ = ConfigValue(opts.Config["underflow"], &underflow) _ = ConfigValue(opts.Config["underflowColor"], &underflowColors) @@ -93,6 +95,7 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { fonts: fonts, colors: colors, frames: frames, + adaptive: adaptive, underflow: underflow, underflowColors: underflowColors, currIndex: 0, @@ -116,17 +119,17 @@ func (w *TimerWidget) Update() error { var fontColor = w.colors[i] if !w.data.HasDeadline() { - str = Timespan(w.times[w.currIndex]).Format(w.formats[i]) + str = Timespan(w.times[w.currIndex]).Format(w.formats[i], w.adaptive) } 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]) + str = Timespan(w.times[w.currIndex]).Format(w.formats[i], w.adaptive) w.data.Clear() } else if remainingDuration < 0 && w.underflow { fontColor = w.underflowColors[i] - str = Timespan(remainingDuration * -1).Format(w.formats[i]) + str = Timespan(remainingDuration*-1).Format(w.formats[i], w.adaptive) } else { - str = Timespan(remainingDuration).Format(w.formats[i]) + str = Timespan(remainingDuration).Format(w.formats[i], w.adaptive) } } font := fontByName(w.fonts[i]) @@ -146,7 +149,8 @@ func (w *TimerWidget) Update() error { type Timespan time.Duration -func (t Timespan) Format(format string) string { +func (t Timespan) Format(format string, adaptive bool) string { + formatStr := format tm := map[string]string{ "%h": "03", "%H": "15", @@ -154,15 +158,46 @@ func (t Timespan) Format(format string) string { "%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() + current := z.Add(time.Duration(t)) + foundNonZero := false + timeStr := "" + if adaptive { + for i := 0; i < len(formatStr); i++ { + if formatStr[i:i+1] == "%" && len(formatStr) > i+1 { + format := ReplaceAll(formatStr[i:i+2], tm) + str := strings.TrimLeft(current.Format(format), "0") + timeStr += str + if str != "" { + format = ReplaceAll(formatStr[i+2:], tm) + timeStr += current.Format(format) + break + } + foundNonZero = true + i++ + } else { + if !foundNonZero { + timeStr += formatStr[i : i+1] + } + } + } + if timeStr == "" { + timeStr = "0" + } + } else { + format := ReplaceAll(format, tm) + timeStr = current.Format(format) } + return timeStr +} - z := time.Unix(0, 0).UTC() - return z.Add(time.Duration(t)).Format(format) +func ReplaceAll(str string, tm map[string]string) string { + for k, v := range tm { + str = strings.ReplaceAll(str, k, v) + } + return str } func (w *TimerWidget) TriggerAction(hold bool) { From 9a23bac9313fa4106b5b1e8a149cf62eb7c44bc4 Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Mon, 14 Feb 2022 11:53:10 +0100 Subject: [PATCH 6/7] Cleanup and documentation --- decks/main.deck | 13 -------- widget_timer.go | 87 ++++++++++++++++++++++++++++++------------------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/decks/main.deck b/decks/main.deck index a7f1ef0..c73c985 100644 --- a/decks/main.deck +++ b/decks/main.deck @@ -60,19 +60,6 @@ [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] diff --git a/widget_timer.go b/widget_timer.go index 2ba1cdf..2f723bb 100644 --- a/widget_timer.go +++ b/widget_timer.go @@ -7,7 +7,7 @@ import ( "time" ) -// TimerWidget is a widget displaying a timer +// TimerWidget is a widget displaying a timer. type TimerWidget struct { *BaseWidget @@ -26,29 +26,34 @@ type TimerWidget struct { data TimerData } +// TimerData represents the current state of the timer. type TimerData struct { startTime time.Time pausedTime time.Time } +// IsPaused returns whether the timer is paused. func (d *TimerData) IsPaused() bool { return !d.pausedTime.IsZero() } +// IsRunning returns whether the timer is running. func (d *TimerData) IsRunning() bool { return !d.IsPaused() && d.HasDeadline() } +// HasDeadline returns whether the start time is set. func (d *TimerData) HasDeadline() bool { return !d.startTime.IsZero() } +// Clear resets the state of the timer. func (d *TimerData) Clear() { d.startTime = time.Time{} d.pausedTime = time.Time{} } -// NewTimerWidget returns a new TimerWidget +// NewTimerWidget returns a new TimerWidget. func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { bw.setInterval(time.Duration(opts.Interval)*time.Millisecond, time.Second/2) @@ -73,6 +78,10 @@ func NewTimerWidget(bw *BaseWidget, opts WidgetConfig) *TimerWidget { times = append(times, defaultDuration) } + if len(formats) == 0 { + formats = append(formats, "%H:%i:%s") + } + layout := NewLayout(int(bw.dev.Pixels)) frames := layout.FormatLayout(frameReps, len(formats)) @@ -113,26 +122,30 @@ func (w *TimerWidget) Update() error { } 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 timespan Timespan var fontColor = w.colors[i] if !w.data.HasDeadline() { - str = Timespan(w.times[w.currIndex]).Format(w.formats[i], w.adaptive) + timespan = Timespan(w.times[w.currIndex]) } 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.adaptive) + if int(w.times[w.currIndex].Seconds()) == 0 { + timespan = Timespan(remainingDuration * -1) + } else if remainingDuration < 0 && !w.underflow { + timespan = Timespan(w.times[w.currIndex]) w.data.Clear() } else if remainingDuration < 0 && w.underflow { fontColor = w.underflowColors[i] - str = Timespan(remainingDuration*-1).Format(w.formats[i], w.adaptive) + timespan = Timespan(remainingDuration * -1) } else { - str = Timespan(remainingDuration).Format(w.formats[i], w.adaptive) + timespan = Timespan(remainingDuration) } } + font := fontByName(w.fonts[i]) + str := timespan.Format(w.formats[i], w.adaptive) drawString(img, w.frames[i], @@ -147,10 +160,11 @@ func (w *TimerWidget) Update() error { return w.render(w.dev, img) } +// Timespan represents the duration between two events. type Timespan time.Duration +// Format returns the formatted version of the timespan. func (t Timespan) Format(format string, adaptive bool) string { - formatStr := format tm := map[string]string{ "%h": "03", "%H": "15", @@ -162,30 +176,9 @@ func (t Timespan) Format(format string, adaptive bool) string { z := time.Unix(0, 0).UTC() current := z.Add(time.Duration(t)) - foundNonZero := false - timeStr := "" + var timeStr string if adaptive { - for i := 0; i < len(formatStr); i++ { - if formatStr[i:i+1] == "%" && len(formatStr) > i+1 { - format := ReplaceAll(formatStr[i:i+2], tm) - str := strings.TrimLeft(current.Format(format), "0") - timeStr += str - if str != "" { - format = ReplaceAll(formatStr[i+2:], tm) - timeStr += current.Format(format) - break - } - foundNonZero = true - i++ - } else { - if !foundNonZero { - timeStr += formatStr[i : i+1] - } - } - } - if timeStr == "" { - timeStr = "0" - } + timeStr = TrimTime(current, format, tm) } else { format := ReplaceAll(format, tm) timeStr = current.Format(format) @@ -193,6 +186,33 @@ func (t Timespan) Format(format string, adaptive bool) string { return timeStr } +// TrimTime does remove leading zeroes and separator that are not required for the time representation. +func TrimTime(current time.Time, formatStr string, tm map[string]string) string { + foundNonZero := false + timeStr := "" + for i := 0; i < len(formatStr); i++ { + if formatStr[i:i+1] == "%" && len(formatStr) > i+1 { + format := ReplaceAll(formatStr[i:i+2], tm) + str := strings.TrimLeft(current.Format(format), "0") + timeStr += str + if str != "" { + format = ReplaceAll(formatStr[i+2:], tm) + timeStr += current.Format(format) + break + } + foundNonZero = true + i++ + } else if !foundNonZero { + timeStr += formatStr[i : i+1] + } + } + if timeStr == "" { + return "0" + } + return timeStr +} + +// ReplaceAll does a replacement with all entries of a map. func ReplaceAll(str string, tm map[string]string) string { for k, v := range tm { str = strings.ReplaceAll(str, k, v) @@ -200,6 +220,7 @@ func ReplaceAll(str string, tm map[string]string) string { return str } +// TriggerAction updates the timer state. func (w *TimerWidget) TriggerAction(hold bool) { if hold { if w.data.IsPaused() { @@ -211,7 +232,7 @@ func (w *TimerWidget) TriggerAction(hold bool) { if w.data.IsRunning() { w.data.pausedTime = time.Now() } else if w.data.IsPaused() && w.data.HasDeadline() { - pausedDuration := time.Now().Sub(w.data.pausedTime) + pausedDuration := time.Since(w.data.pausedTime) w.data.startTime = w.data.startTime.Add(pausedDuration) w.data.pausedTime = time.Time{} } else { From 6a8c467a9d67abef2aac8961879f86dc2ffcc7c9 Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Thu, 21 Dec 2023 12:45:38 +0100 Subject: [PATCH 7/7] Fix deprecated goreleaser syntax and drop nfpm in this repo for now --- .goreleaser.yml | 50 +++++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 52d2abb..2502cac 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,36 +20,28 @@ builds: - 7 archives: - - replacements: - 386: i386 - amd64: x86_64 + - name_template: >- + {{- .ProjectName }}_ + {{- .Version }}_ + {{- .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end -}} -nfpms: - - builds: - - deckmaster - vendor: muesli - homepage: "https://fribbledom.com/" - maintainer: "Christian Muehlhaeuser " - description: "An application to control your Elgato Stream Deck" - license: MIT - formats: - - apk - - deb - - rpm - bindir: /usr/bin - -#brews: -# - goarm: 6 -# tap: -# owner: muesli -# name: homebrew-tap -# commit_author: -# name: "Christian Muehlhaeuser" -# email: "muesli@gmail.com" -# homepage: "https://fribbledom.com/" -# description: "An application to control your Elgato Stream Deck" -# dependencies: -# - name: linux +# nfpms: +# - builds: +# - deckmaster +# vendor: muesli +# homepage: "https://fribbledom.com/" +# maintainer: "Christian Muehlhaeuser " +# description: "An application to control your Elgato Stream Deck" +# license: MIT +# formats: +# - apk +# - deb +# - rpm +# bindir: /usr/bin signs: - artifacts: checksum