feat: initial gosleep-timer implementation

Go TUI sleep timer with modular architecture.
- Config: YAML-based, profile inheritance, auto WM detection
- Engine: timer, executor, stage pipeline (pre→timer→post)
- Modules: workspace (niri/hyprland/kde), media, lock, kill,
  notify, sound, brightness, mute, custom, script
- TUI: bubbletea app with profiles, module toggles, progress bar
- CLI: run, list-profiles, export, import, history, stats
- History: JSONL log with statistics
- QR: config export as ASCII QR
This commit is contained in:
2026-07-03 01:43:22 +02:00
commit 8501fd074c
34 changed files with 3401 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
package engine
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
type Status int
const (
StatusIdle Status = iota
StatusRunning
StatusPaused
StatusDone
StatusKilled
StatusError
)
type StageType string
const (
StagePre StageType = "pre"
StageTimer StageType = "timer"
StagePost StageType = "post"
)
type TimerEvent struct {
Remaining time.Duration
Total time.Duration
Stage StageType
Status Status
Done bool
}
type Timer struct {
Duration time.Duration
Status Status
StartedAt time.Time
elapsed time.Duration
cancel context.CancelFunc
}
func ParseDuration(s string) (time.Duration, error) {
s = strings.TrimSpace(s)
if d, err := time.ParseDuration(s); err == nil {
return d, nil
}
re := regexp.MustCompile(`(\d+)([hms])`)
matches := re.FindAllStringSubmatch(s, -1)
if len(matches) == 0 {
return 0, fmt.Errorf("invalid duration: %s", s)
}
var d time.Duration
for _, m := range matches {
v, _ := strconv.Atoi(m[1])
switch m[2] {
case "h":
d += time.Duration(v) * time.Hour
case "m":
d += time.Duration(v) * time.Minute
case "s":
d += time.Duration(v) * time.Second
}
}
return d, nil
}
func NewTimer(d time.Duration) *Timer {
return &Timer{Duration: d, Status: StatusIdle}
}
func (t *Timer) Start(ctx context.Context, events chan<- TimerEvent) {
ctx, cancel := context.WithCancel(ctx)
t.cancel = cancel
t.StartedAt = time.Now()
t.Status = StatusRunning
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
t.Status = StatusKilled
events <- TimerEvent{Status: StatusKilled, Done: true}
return
case <-ticker.C:
elapsed := time.Since(t.StartedAt)
remaining := t.Duration - elapsed
if remaining <= 0 {
t.Status = StatusDone
events <- TimerEvent{Remaining: 0, Total: t.Duration, Status: StatusDone, Done: true}
return
}
events <- TimerEvent{
Remaining: remaining,
Total: t.Duration,
Stage: StageTimer,
Status: StatusRunning,
}
}
}
}()
}
func (t *Timer) Stop() {
if t.cancel != nil {
t.cancel()
}
t.Status = StatusKilled
}