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:
@@ -0,0 +1,42 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExecResult struct {
|
||||
Cmd string
|
||||
Output string
|
||||
Err error
|
||||
Elapsed time.Duration
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context, cmd string, timeout time.Duration) ExecResult {
|
||||
start := time.Now()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if timeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
out, err := exec.CommandContext(ctx, "bash", "-c", cmd).CombinedOutput()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
return ExecResult{
|
||||
Cmd: cmd,
|
||||
Output: string(out),
|
||||
Err: err,
|
||||
Elapsed: elapsed,
|
||||
}
|
||||
}
|
||||
|
||||
func ExecuteAll(ctx context.Context, cmds []string, timeout time.Duration) []ExecResult {
|
||||
results := make([]ExecResult, len(cmds))
|
||||
for i, cmd := range cmds {
|
||||
results[i] = Execute(ctx, cmd, timeout)
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StageRunner struct {
|
||||
Timer *Timer
|
||||
PreCmds []string
|
||||
PostCmds []string
|
||||
Events chan TimerEvent
|
||||
ExecTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewStageRunner(timer *Timer, pre, post []string) *StageRunner {
|
||||
return &StageRunner{
|
||||
Timer: timer,
|
||||
PreCmds: pre,
|
||||
PostCmds: post,
|
||||
Events: make(chan TimerEvent, 100),
|
||||
ExecTimeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *StageRunner) Run(ctx context.Context) {
|
||||
defer close(sr.Events)
|
||||
|
||||
// PreCmds
|
||||
for _, cmd := range sr.PreCmds {
|
||||
result := Execute(ctx, cmd, sr.ExecTimeout)
|
||||
if result.Err != nil {
|
||||
sr.Events <- TimerEvent{Stage: StagePre, Status: StatusError, Done: true}
|
||||
return
|
||||
}
|
||||
sr.Events <- TimerEvent{Stage: StagePre, Status: StatusRunning}
|
||||
}
|
||||
|
||||
// Timer
|
||||
timerEvents := make(chan TimerEvent, 100)
|
||||
go sr.Timer.Start(ctx, timerEvents)
|
||||
|
||||
timerDone := false
|
||||
timerLoop:
|
||||
for {
|
||||
select {
|
||||
case ev := <-timerEvents:
|
||||
sr.Events <- ev
|
||||
if ev.Done {
|
||||
timerDone = true
|
||||
break timerLoop
|
||||
}
|
||||
case <-ctx.Done():
|
||||
break timerLoop
|
||||
}
|
||||
}
|
||||
|
||||
// PostCmds (only on normal timer completion)
|
||||
if timerDone && sr.Timer.Status == StatusDone {
|
||||
for _, cmd := range sr.PostCmds {
|
||||
result := Execute(ctx, cmd, sr.ExecTimeout)
|
||||
if result.Err != nil {
|
||||
sr.Events <- TimerEvent{Stage: StagePost, Status: StatusError, Done: true}
|
||||
return
|
||||
}
|
||||
sr.Events <- TimerEvent{Stage: StagePost, Status: StatusRunning}
|
||||
}
|
||||
sr.Events <- TimerEvent{Status: StatusDone, Done: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *StageRunner) Stop() {
|
||||
sr.Timer.Stop()
|
||||
}
|
||||
+116
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user