🐡

1日でシンプルなアラーム機能のCLIをGoで作った話

2024/01/08に公開

はじめに

こんにちは、Web エンジニアの tomoです。

皆さんは仕事中、たとえば「あと 30 分で会議が始まる」というような時、どうしていますか?
僕は、仕事に集中しちゃって忘れることが怖いので、よくスマホのアラームをセットしています。

ただ、いちいちスマホのアラームをセットするのは、エンジニアの僕からするとちょっと面倒なんですよね。
ということで、CLI でアラームをセットできたら便利だと思い、Go の勉強がてら作ってみました。

作成した「goalarm-cli」の概要説明

GitHub のリポジトリはこちらです。

https://github.com/tomo-kn/goalarm-cli

まず前提として、Go の開発環境が整っていることが必要です。
公式ドキュメントを参考に、Go をインストールしておいてください。

https://go.dev/doc/install

次に、go install コマンドで goalarm-cli をインストールします。

$ go install github.com/tomo-kn/goalarm-cli/cmd/goalarm@latest

これで、goalarm コマンドが使えるようになります。

$ goalarm

使い方

アラームをセットするには、goalarm set [TIME] コマンドを使います。

$ goalarm set 23:18

これで、23:18 にアラームが鳴ります。

アラームが鳴ったら Enter キーを押すと、アラームが止まります。

アラームが鳴る前に止めたい場合は、Ctrl + C で止めてください。

実際に使ってみた様子はこんな感じです。

使用時は音が出るので、PC の音量にはくれぐれもご注意ください 🙏

一応、アラームの消し忘れ防止のため、15 分間アラームが鳴り続けると自動で止まる機能をつけています。

仕組み

全 115 行のコードで実装できました。

package main

import (
	"bufio"
	"embed"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/gopxl/beep"
	"github.com/gopxl/beep/speaker"
	"github.com/gopxl/beep/wav"
	"github.com/spf13/cobra"
	"golang.org/x/term"
)

//go:embed assets/alarm.wav
var alarmSound embed.FS

func main() {
	var rootCmd = &cobra.Command{Use: "goalarm"}

	var setCmd = &cobra.Command{
		Use:   "set [time]",
		Short: "Set an alarm",
		Args:  cobra.MinimumNArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
				setTime(args[0])
		},
	}


	rootCmd.AddCommand(setCmd)
	rootCmd.Execute()
}

func setTime(timeStr string) {
	layout := "15:04"
	now := time.Now()

	targetTime, err := time.ParseInLocation(layout, timeStr, now.Location())
	if err != nil {
		fmt.Println("Invalid time format. Please use HH:MM.")
		return
	}

	targetTime = time.Date(now.Year(), now.Month(), now.Day(), targetTime.Hour(), targetTime.Minute(), 0, 0, now.Location())

	if targetTime.Before(now) {
		targetTime = targetTime.Add(24 * time.Hour)
		fmt.Printf("Alarm set for %s (tomorrow)\n", targetTime.Format(layout))
	} else {
		fmt.Printf("Alarm set for %s (today)\n", targetTime.Format(layout))
	}

	diff := targetTime.Sub(now)
	fmt.Println("Waiting for alarm... Press Ctrl + C to cancel.")

	time.Sleep(diff)

	done := make(chan bool)
	go playAlarmSound(done)

	fmt.Println("Alarm! The time is now", targetTime.Format(layout))
	fmt.Println("Press 'Enter' to stop the alarm.")

	oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		panic(err)
	}
	defer term.Restore(int(os.Stdin.Fd()), oldState)

	reader := bufio.NewReader(os.Stdin)
	for {
		char, _, err := reader.ReadRune()
		if err != nil {
			panic(err)
		}
		if char == '\r' { // Enter pressed
			done <- true
			break
		}
	}
}

func playAlarmSound(done chan bool) {
	alarmFile, err := alarmSound.Open("assets/alarm.wav")
	if err != nil {
    log.Fatal(err)
	}
	defer alarmFile.Close()

	streamer, format, err := wav.Decode(alarmFile)
	if err != nil {
		log.Fatal(err)
	}
	defer streamer.Close()

	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

	loop := beep.Loop(-1, streamer)

	go func() {
		select {
		case <-done:
		case <-time.After(15 * time.Minute):
			fmt.Println("15 minutes have passed, stopping the alarm automatically")
      os.Exit(0)
		}
		speaker.Clear() // Clear the speaker to stop playing
	}()

	speaker.Play(loop)
}

アラームが鳴ってからの Enter のみ入力を受け付けるために、term パッケージを使っています。

そこが若干苦労したポイントですが、基本的には、setTime 関数でアラームをセットして、playAlarmSound 関数でアラームを鳴らしているだけです。

Go には cobra という CLI フレームワークがあるので、それを使ってコマンドを作っています。

こんなに簡単に CLI が作れるなんて思ってなかったので、割と感動しました。

まとめ

Go の勉強がてら、1 日でシンプルなアラーム機能の CLI を作ってみました。

「アラームを設定したいけど、スマホのアプリを使うまでではないな...」という時に結構便利です。

僕も最近はこの goalarm を愛用しています笑。

よろしければ、ぜひ使ってみてください!

参考文献

公式ドキュメント

GitHub リポジトリ

ブログ記事

GitHubで編集を提案

Discussion