はじめに
普段、開発作業はほとんどターミナルで行っています。集中力を維持するためにポモドーロテクニックを愛用しているのですが、既存のタイマーツールは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コマンドによって起動される、独立したバックグラウンドプロセスです。その主な責務は以下の通りです。
-
タイマーロジックの実行:
internal/daemon/timer.goにあるTimer構造体が、ポモドーロのセッション管理(状態、残り時間、サイクルの計算など)のすべてを担います。1秒ごとにTick()メソッドが呼ばれ、状態を更新します。 -
RPCサービスの提供: クライアントからの指示を受け付けるために、Go標準の
net/rpcパッケージを利用したRPCサーバーを起動します。これにより、Start,Stop,Statusといった関数を外部のプロセス(クライアント)から安全に呼び出すことが可能になります。 -
プロセス間通信 (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を通じて対話します。
-
接続 (Dial): クライアント (
internal/client/client.go) は、デーモンが作成したUnixソケットに接続します。// internal/ipc/ipc.go func Dial() (net.Conn, error) { return net.Dial("unix", GetSocketPath()) } -
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 } -
サービス実行: デーモン側では、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を選びました。サブコマンドの管理やフラグの定義が非常に簡単になります。
例えば、ルートコマンドであるpmdrは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) では、タイマーの時間を上書きするためのフラグを定義しています。
// ...
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つのソースから設定値をいい感じにマージしてくれます。
- 設定ファイル (YAML)
- 環境変数
- コマンドラインフラグ
優先度は「フラグ > 環境変数 > 設定ファイル」となっており、ユーザーは自分に合った方法で設定を上書きできます。
cmd/root.goのinitConfig関数で、viperの初期設定を行っています。
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の裏側にあるアーキテクチャや、cobraとviperを使った開発の工夫について紹介しました。
ぜひ一度使ってみて、フィードバックや改善提案など、GitHubリポジトリのIssueやPull Requestでお待ちしています!