0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ポモドーロタイマーをGoで実装した話

0
Posted at

はじめに

普段、開発作業はほとんどターミナルで行っています。集中力を維持するためにポモドーロテクニックを愛用しているのですが、既存のタイマーツールはGUIベースのものが多く、作業の流れが中断されがちでした。

「ターミナルから離れずに、もっとシンプルで、自分好みに連携できるタイマーが欲しい!」

そんな思いから、Go言語で自作のポモドーロタイマー「pmdr」を開発しました。この記事では、pmdrがどのようなツールで、どういった技術的な工夫がされているのか、その仕組みを解説したいと思います。

pmdrとは?

pmdrは、バックグラウンドで動作する、ターミナル向けのシンプルなポモドーロタイマーです。

# 新しいポモドーロセッションを開始
$ pmdr start
time=2025-08-24T12:02:01.141+09:00 level=INFO msg="Daemon not running, starting it now..."
time=2025-08-24T12:02:01.645+09:00 level=INFO msg="Daemon started."
time=2025-08-24T12:02:01.649+09:00 level=INFO msg="Pomodoro session started."

# ステータスを確認
$ pmdr status
time=2025-08-24T12:02:17.104+09:00 level=INFO msg="[Running] Work 00:24:45 (ends at 12:27:01) (Cycle 1)"

# 一時停止
$ pmdr pause
time=2025-08-24T12:02:38.487+09:00 level=INFO msg="Pomodoro session paused."

# 再開
$ pmdr resume
time=2025-08-24T12:03:08.498+09:00 level=INFO msg="Pomodoro session resumed."

# 停止
time=2025-08-24T12:03:25.885+09:00 level=INFO msg="Pomodoro session stopped."

主な特徴:

  • デーモンベース: 一度起動すれば、ターミナルを閉じてもタイマーは動き続けます。
  • シンプルなCLI: start, status, pauseなど、直感的なコマンドで操作できます。cobraベースなのでターミナルでの補完機能も充実しています。
  • 柔軟な設定: YAMLファイル、環境変数、コマンドラインフラグでタイマーの時間などを細かく設定できます。
  • フック機能: セッションの切り替え時に好きなシェルコマンドを実行でき、デスクトップ通知など自由な連携が可能です。

アーキテクチャ詳解:デーモンとRPC通信の裏側

pmdrの要は、バックグラウンドで動き続ける「デーモン」と、ユーザーが操作する「CLIクライアント」が分離されたクライアント・サーバー型のアーキテクチャです。ここでは、その心臓部であるデーモンと、クライアントとの通信方法について詳しく見ていきましょう。

デーモンプロセス(サーバー)

デーモンは、pmdr daemonコマンドによって起動される、独立したバックグラウンドプロセスです。その主な責務は以下の通りです。

  1. タイマーロジックの実行: internal/daemon/timer.goにあるTimer構造体が、ポモドーロのセッション管理(状態、残り時間、サイクルの計算など)のすべてを担います。1秒ごとにTick()メソッドが呼ばれ、状態を更新します。

  2. RPCサービスの提供: クライアントからの指示を受け付けるために、Go標準のnet/rpcパッケージを利用したRPCサーバーを起動します。これにより、Start, Stop, Statusといった関数を外部のプロセス(クライアント)から安全に呼び出すことが可能になります。

  3. プロセス間通信 (IPC) インターフェース: 通信には、高速でオーバーヘッドの少ないUnixソケットを利用します。デーモンは起動時に/tmp/pmdr.sockのようなソケットファイルを作成し、クライアントからの接続を待ち受けます。

デーモンの起動シーケンスは以下のようになっています (internal/daemon/daemon.go)。

func Run() error {
	// 1. 自身のプロセスIDをファイルに書き出す (後で安全に停止するため)
	pidPath := ipc.GetPidPath()
	os.WriteFile(pidPath, []byte(strconv.Itoa(os.Getpid())), 0644)

	// 2. TimerロジックとRPCサービスを初期化
	timer := NewTimer(cfg)
	service := NewPmdrService(timer)
	rpc.RegisterName(ipc.ServiceName, service)

	// 3. Unixソケットでリッスンを開始
	socketPath := ipc.GetSocketPath()
	listener, err := net.Listen("unix", socketPath)

	// 4. シグナルを待って安全にシャットダウンするゴルーチンを起動
	go handleSignals(listener)

	// 5. クライアントからの接続を待ち、接続ごとにゴルーチンでRPCを処理
	for {
		conn, err := listener.Accept()
		go rpc.ServeConn(conn)
	}
}

クライアント・サーバー間のRPC通信

ユーザーがpmdr statusのようなコマンドを実行すると、クライアントはデーモンとRPCを通じて対話します。

  1. 接続 (Dial): クライアント (internal/client/client.go) は、デーモンが作成したUnixソケットに接続します。

    // internal/ipc/ipc.go
    func Dial() (net.Conn, error) {
        return net.Dial("unix", GetSocketPath())
    }
    
  2. RPC呼び出し (Call): 接続が確立されると、クライアントはPmdrService.Statusのような形式で、デーモンに登録されたサービスメソッドを呼び出します。引数と戻り値の型はinternal/ipc/ipc.goで共有されています。

    // internal/client/client.go
    func Status() (*ipc.StatusReply, error) {
        var reply ipc.StatusReply
        // "PmdrService.Status"を呼び出し、結果をreplyに格納
        err := call(ipc.ServiceName+".Status", &ipc.Args{}, &reply)
        return &reply, err
    }
    
  3. サービス実行: デーモン側では、RPCリクエストを受け取ると、対応するPmdrServiceのメソッドが実行され、現在のタイマーの状態をクライアントに返します。

    // internal/daemon/rpc.go
    func (s *PmdrService) Status(args *ipc.Args, reply *ipc.StatusReply) error {
        *reply = s.timer.Status()
        return nil
    }
    

このように、RPCを介してコンポーネントを疎結合に保っています。

cobraによるコマンド管理

CLIのフレームワークには、KubernetesやGitHub CLIなど、多くの大規模プロジェクトで採用されているcobraを選びました。サブコマンドの管理やフラグの定義が非常に簡単になります。

例えば、ルートコマンドであるpmdrcmd/root.goでこのように定義されています。

cmd/root.go
// ...
var RootCmd = &cobra.Command{
	Use:   "pmdr",
	Short: "A simple Pomodoro timer for your terminal",
	// ...
}

func init() {
	// サブコマンドを追加
	RootCmd.AddCommand(StartCmd)
	RootCmd.AddCommand(StatusCmd)
	// ...

	// 全コマンドで使える永続フラグ
	RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
}
// ...

startコマンド (cmd/start.go) では、タイマーの時間を上書きするためのフラグを定義しています。

cmd/start.go
// ...
var StartCmd = &cobra.Command{
	Use:   "start",
	Short: "Starts a new Pomodoro session",
	RunE: func(cmd *cobra.Command, args []string) error {
		// フラグの値を取得してデーモンに渡す
		// ...
	},
}

func init() {
	RootCmd.AddCommand(StartCmd)

	// startコマンド固有のフラグ
	StartCmd.Flags().StringP("work", "w", "", "Work session duration (e.g., 25m)")
	// ...
}

このように、cobraを使うことで、コマンドの構造をきれいに保ちながら、拡張性の高いCLIを効率的に開発できます。

柔軟な設定の実現: viperの活用

タイマーの時間設定などを柔軟に行えるようにするため、viperというライブラリを導入しました。viperは、以下の3つのソースから設定値をいい感じにマージしてくれます。

  1. 設定ファイル (YAML)
  2. 環境変数
  3. コマンドラインフラグ

優先度は「フラグ > 環境変数 > 設定ファイル」となっており、ユーザーは自分に合った方法で設定を上書きできます。

cmd/root.goinitConfig関数で、viperの初期設定を行っています。

cmd/root.go
func initConfig() {
	// 1. 設定ファイルを探して読み込む
	foundCfgFile, err := configInternal.FindConfigFile(cfgFile)
	if foundCfgFile != "" {
		viper.SetConfigFile(foundCfgFile)
		_ = viper.ReadInConfig()
	}

	// 2. 環境変数を読み込む
	viper.AutomaticEnv()

	// 3. フラグとViperを紐付ける (init()内)
	// _ = vip.BindPFlag("log.level", RootCmd.PersistentFlags().Lookup("log-level"))
}

この仕組みにより、プロジェクトのルートに.pmdr.yamlを置くだけでなく、PMDR_WORK_DURATION=30m pmdr startのように環境変数で挙動を変えることも可能です。

インストールと使い方

Go環境があれば、すぐにインストールできます。

go install github.com/tsuperis3112/pmdr@latest

詳しい使い方はREADME.mdを参照してください。

おわりに

この記事では、自作のポモドーロタイマーpmdrの裏側にあるアーキテクチャや、cobraviperを使った開発の工夫について紹介しました。

ぜひ一度使ってみて、フィードバックや改善提案など、GitHubリポジトリのIssueやPull Requestでお待ちしています!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?