summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2024-03-28 05:20:49 +0700
committerShulhan <ms@kilabit.info>2024-04-03 02:12:58 +0700
commit632d208c7cff484a651150e78fcadb827e69f1b8 (patch)
tree4f972bbedaf0cf41b8825ee629ab6fd3f11f86bf
parent14325589db35cf36ed1aa71ff4f2c5ad0bb6886b (diff)
downloadpakakeh.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--.gitignore1
-rw-r--r--cmd/ansua/README.md41
-rw-r--r--cmd/ansua/main.go198
3 files changed, 240 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 0aeea614..7faf3c6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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()
+}