はじめに
ポモドーロタイマー+記録の自動化をするためのデスクトップアプリを作りました。
ポモドーロテクニックは知っていたけれど、どうにも使う気になれませんでした。
記録を別のノートアプリに手動でコピーするのが億劫というのが大きかったと思います。
そこで、ポモドーロテクニックを続けるために、マウスポインタの方向を示す機能と
obsidianのデイリーノートへの連携を追加しました。
ポモドーロ完了のたびにObsidianのデイリーノートへ自動記録する機能も実装しています。
完成品
PomoTimerDesk — ミニマルな半透明ポモドーロタイマー

- 半透明・常に最前面表示で作業の邪魔にならない
- マウスポインタをアプリ内のリングに反映(アプリ外でも追従)
- 作業完了時にObsidianデイリーノートへ自動記録
- 通知音のカスタム設定対応
技術スタック
| レイヤー | 技術 | バージョン |
|---|---|---|
| UIフレームワーク | React | 19 |
| 言語 | TypeScript | 5 |
| バンドラー | Vite | 7 |
| デスクトップランタイム | Tauri | v2 |
| バックエンド | Rust | latest stable |
| テスト | Vitest | 4 |
Tauri v2はRustをバックエンドに使うデスクトップフレームワークです。Electronと比べてバイナリサイズが小さく、メモリ使用量も抑えられます。
主な機能
1. マウスポインタの追従
円形プログレスリングの内側に、現在のマウス位置をドットで表示します。アプリウィンドウの外にカーソルがあっても追従します。
通常のWebブラウザではウィンドウ外のカーソル座標を取れませんが、Tauriを使うとRust側からシステムのカーソル座標を取得してフロントエンドに渡せます。
Rust側(src-tauri/src/lib.rs)
#[tauri::command]
fn get_cursor_pos(window: tauri::WebviewWindow) -> Result<(f64, f64), String> {
let pos = window.cursor_position().map_err(|e| e.to_string())?;
Ok((pos.x, pos.y))
}
TypeScript側
const pos = await invoke<[number, number]>("get_cursor_pos");
// pos[0] = x, pos[1] = y (物理スクリーン座標)
invoke はTauri IPCのフロントエンドAPIで、Rustのコマンドを非同期で呼び出せます。
2. 背景透明度の動的制御
設定画面のスライダーでリアルタイムに透明度を変更できます。CSS変数をReactから動的に書き換えることで実現しています。
3. 通知音のカスタム設定
デフォルトのベル音に加えて、任意のフォルダにある音声ファイル(mp3 / wav / ogg / m4a / flac)を選択できます。
const selected = await open({
directory: true,
multiple: false,
title: "通知音フォルダを選択",
});
4. Obsidianデイリーノートへの自動記録【メイン機能】
ポモドーロ完了時に、指定したObsidian VaultのMarkdownファイルへ作業時刻を自動追記します。

記録される内容のイメージ(デイリーノート 2026-03-28.md)
- work: 10:25
- work: 11:00
- work: 14:30
Rust側の実装
#[tauri::command]
fn append_to_daily_note(
vault_path: String,
daily_notes_folder: Option<String>,
date_format: String,
folder_structure: String,
work_label: String,
) -> Result<(), String> {
let now = Local::now();
let date_str = now.format(&date_format).to_string();
let time_str = now.format("%H:%M").to_string();
let mut path = PathBuf::from(&vault_path);
if let Some(folder) = daily_notes_folder {
if !folder.trim().is_empty() {
path.push(&folder);
}
}
if folder_structure == "year-month" {
path.push(now.format("%Y").to_string());
path.push(now.format("%m").to_string());
}
std::fs::create_dir_all(&path).map_err(|e| e.to_string())?;
path.push(format!("{}.md", date_str));
let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
writeln!(file, "- {}: {}", work_label, time_str)?;
Ok(())
}
設定できる項目:
| 設定項目 | 説明 | デフォルト |
|---|---|---|
| Vaultのパス | ObsidianのVaultフォルダ | — |
| Daily Notesフォルダ | Vault内のサブフォルダ | (ルート直下) |
| 日付フォーマット | ファイル名の日付形式 | %Y-%m-%d |
| フォルダ構造 | フラット or 年/月サブフォルダ | flat |
| 作業ラベル | 記録する文字列 | work |
ファイルが存在しない場合は自動作成され、存在する場合は末尾に追記されます。create_dir_all によってフォルダも自動生成されるため、新しい月になっても設定変更不要です。
設定の永続化
設定はlocalStorageに保存しています。Tauriのデスクトップアプリでもブラウザと同じlocalStorage APIが使えます。
// 変更のたびに自動保存
useEffect(() => {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
}, [settings]);
// 起動時に読み込み
const [settings, setSettings] = useState<AppSettings>(() => {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
return stored ? { ...DEFAULT_SETTINGS, ...JSON.parse(stored) } : DEFAULT_SETTINGS;
});
`Partial<AppSettings>` を使った部分更新パターンにより、設定の一部だけを変更しても他の値が失われません。
```typescript
const updateSettings = (patch: Partial<AppSettings>) => {
setSettings(prev => ({ ...prev, ...patch }));
};
Partial を使った部分更新パターンにより、設定の一部だけを変更しても他の値が失われません。
おわりに
Tauri v2 + React + TypeScriptの組み合わせで、Webの開発体験を保ちながらネイティブに近い機能(ファイルI/O、グローバルカーソル取得)を実現できました。
特にObsidian連携は、記録するコストがゼロになったことでタイマーを回すこと自体が習慣化しやすくなったと感じています。
ご自身でビルドして使っていただくことも可能です。前提条件は Node.js (v18+) と Rust (latest stable) のみです。
git clone https://github.com/MameMame777/PomoTimerDesk.git
cd PomoTimerDesk
npm install
npm run tauri build
ビルドが完了すると src-tauri/target/release/bundle/nsis/ にインストーラーが生成されます。
ぜひ使ってみてください。
GitHub: https://github.com/MameMame777/PomoTimerDesk

