diff options
| author | Shulhan <ms@kilabit.info> | 2024-03-28 05:20:49 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-04-03 02:12:58 +0700 |
| commit | 632d208c7cff484a651150e78fcadb827e69f1b8 (patch) | |
| tree | 4f972bbedaf0cf41b8825ee629ab6fd3f11f86bf | |
| parent | 14325589db35cf36ed1aa71ff4f2c5ad0bb6886b (diff) | |
| download | pakakeh.go-632d208c7cff484a651150e78fcadb827e69f1b8.tar.xz | |
cmd/ansua: command line interface to help tracking time
Usage,
ansua <duration> [ "<command>" ]
ansua execute a timer on defined duration and optionally run a command
when timer finished.
When ansua timer is running, one can pause the timer by pressing p+Enter,
and resume it by pressing r+Enter, or stopping it using CTRL+c.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | cmd/ansua/README.md | 41 | ||||
| -rw-r--r-- | cmd/ansua/main.go | 198 |
3 files changed, 240 insertions, 0 deletions
@@ -15,6 +15,7 @@ *.stats *.test *.zst +/_bin/ansua /_bin/bcrypt /_bin/epoch /_bin/gofmtcomment diff --git a/cmd/ansua/README.md b/cmd/ansua/README.md new file mode 100644 index 00000000..b2ae6c80 --- /dev/null +++ b/cmd/ansua/README.md @@ -0,0 +1,41 @@ +# ansua + +ansua is a command line interface to help tracking time. + +## SYNOPSIS + + ansua <duration> [ "<command>" ] + +## DESCRIPTION + +ansua execute a timer on defined duration and optionally run a command when +timer finished. + +When ansua timer is running, one can pause the timer by pressing p+Enter, +and resume it by pressing r+Enter, or stopping it using CTRL+c. + +## PARAMETERS + +The duration parameter is using the "XhYmZs" format, where "h" represent +hours, "m" represent minutes, and "s" represent seconds. +For example, "1h30m" equal to one hour 30 minutes, "10m30" equal to 10 +minutes and 30 seconds. + +The command parameter is optional. + +## EXAMPLE + +Run timer for 1 minute, + + $ ansua 1m + +Run timer for 1 hour and then display notification using notify-send in +GNU/Linux, + + $ ansua 1h notify-send "ansua completed" + +## LINKS + +Repository: https://git.sr.ht/~shulhan/pakakeh.go + +Issue: https://todo.sr.ht/~shulhan/pakakeh.go diff --git a/cmd/ansua/main.go b/cmd/ansua/main.go new file mode 100644 index 00000000..5913807b --- /dev/null +++ b/cmd/ansua/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "bufio" + "context" + _ "embed" + "flag" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" +) + +//go:embed README.md +var readme string + +const ( + opHelp = `help` +) + +const ( + stateCompleted = `completed` + statePaused = `paused` + stateRunning = `running` +) + +const defTickerDuration = 20 * time.Second + +func main() { + flag.Parse() + + var param1 = flag.Arg(0) + + if len(param1) == 0 { + fmt.Println(readme) + os.Exit(1) + } + + param1 = strings.ToLower(param1) + if param1 == opHelp { + fmt.Println(readme) + os.Exit(0) + } + + var ( + dur time.Duration + err error + ) + + dur, err = time.ParseDuration(param1) + if err != nil { + log.Fatalf(`%s: %s`, os.Args[0], err) + } + + var execArgs = getExecArg() + + fmt.Printf(`Running for %s`, dur) + if len(execArgs) != 0 { + fmt.Printf(` and then execute command %q`, execArgs) + } + fmt.Println(`.`) + + var ( + orgDur = dur + timeStart = time.Now().Round(time.Second) + ticker = time.NewTicker(defTickerDuration) + timer = time.NewTimer(dur) + signalq = make(chan os.Signal, 1) + inputq = make(chan byte, 1) + state = stateRunning + + pressed byte + ) + + signal.Notify(signalq, os.Interrupt, syscall.SIGTERM) + + go readKey(inputq) + + for state == stateRunning { + select { + case <-signalq: + onStopped(`[Terminated]`, orgDur, timeStart) + timer.Stop() + os.Exit(0) + + case <-timer.C: + state = stateCompleted + dur = 0 + + case <-ticker.C: + dur -= defTickerDuration + fmt.Printf("% 9s remaining...\n", dur) + + case pressed = <-inputq: + if pressed == 'p' { + onStopped(`[Paused]`, orgDur, timeStart) + timer.Stop() + ticker.Stop() + state = statePaused + } + } + for state == statePaused { + select { + case <-signalq: + onStopped(`[Terminated]`, orgDur, timeStart) + os.Exit(0) + case pressed = <-inputq: + if pressed == 'r' { + fmt.Println(`[Resumed]`) + timer = time.NewTimer(dur) + ticker = time.NewTicker(defTickerDuration) + state = stateRunning + } + } + } + } + + fmt.Println(`Time completed.`) + + if len(execArgs) == 0 { + return + } + + fmt.Println(`Executing command...`) + run(signalq, execArgs) +} + +func getExecArg() (execArgs string) { + var args = flag.Args()[1:] + if len(args) == 0 { + // No command provided, exit immediately. + return `` + } + + return strings.Join(args, ` `) +} + +func onStopped(cause string, orgDur time.Duration, timeStart time.Time) { + var dur = orgDur - time.Now().Sub(timeStart).Round(time.Second) + fmt.Printf("%s remaining duration is %s.\n", cause, dur) +} + +func readKey(inputq chan byte) { + var ( + in = bufio.NewReader(os.Stdin) + + err error + c byte + ) + fmt.Println(`Press and enter [p] to pause, [r] to resume.`) + for { + c, err = in.ReadByte() + if c == 0 { + continue + } + if err != nil { + log.Println(err) + } + if c == 'p' || c == 'r' { + inputq <- c + } + } +} + +func run(signalq chan os.Signal, execArgs string) { + var ( + ctx context.Context + ctxCancel context.CancelFunc + ) + + ctx, ctxCancel = context.WithCancel(context.Background()) + + var execCmd = exec.CommandContext(ctx, `/bin/sh`, `-c`, execArgs) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + var done = make(chan struct{}, 1) + + go func() { + var err2 = execCmd.Run() + if err2 != nil { + log.Printf(`%s: %s`, os.Args[0], err2) + } + done <- struct{}{} + }() + + select { + case <-signalq: + ctxCancel() + os.Exit(0) + case <-done: + } + ctxCancel() +} |
