Hitoiki(一息) という常駐アプリを作り、GitHub で公開しました。macOS と Windows で動きます。設定した時刻になると全画面が数回光り、そのあと休憩が終わるまで画面が暗いオーバーレイで覆われます。
休憩の通知は、過集中している自分には届かなかった
私は ASD と ADHD を持っていて、PC で作業をしているとよく過集中に入ります。
コードを書いているときは特にそうで、気づくと何時間も経っています。
集中できるのは悪いことばかりでもありませんが、休憩を挟まずに何時間も続けるのは体によくありません。実際、昼食をとるのを失念してしまい、作業を続けてしまったりすることがよくあります。
対策として、スケジュール管理アプリでリマインダーを設定したり、ポモドーロ・テクニックのアプリを入れたりもしました。でも、私にはあまり効きませんでした。
通知が画面の隅に出ても、過集中しているときはほとんど目に入りません。
目に入っても、内容を読まずに閉じてしまいます。「あと少しだけ」と思ったまま、次の通知まで作業を続けていることもよくありました。
休んだほうがいいことは、自分でも分かっていました。
困るのは、通知が目立たないことです。
「閉じるのに手間がかかれば、さすがに一度は手が止まるだろう」
そう考えて作ったのが Hitoiki です。
画面を光らせて、そのまま覆う
休憩の時刻が来ると、Hitoiki は次の順に動きます。
- フラッシュ: 休憩の時刻になると、つないでいる全モニターが数回白く光ります。画面の隅の通知と違って、見落としようがありません。
- 休憩オーバーレイ: 光ったあと、全モニターを半透明の黒で覆い、中央に大きなカウントダウンを出します。キー入力はオーバーレイが受け取るので、エディタやブラウザには届きません。
カウントダウンが 0 になると、オーバーレイは自動で消えて元の作業に戻れます。どうしても今すぐ戻りたいときは、Esc を 3 秒長押しすると解除されます。うっかり触れて解除されないよう、短く押しただけでは反応しません。押している間は進捗がリングで出ます。
休憩のタイミングは「50 分ごと」のような間隔でも、「15:00」のような時刻でも指定でき、両方を併用することもできます。時刻ごとに休憩の長さを変えられるので、昼の 12 時だけ 1 時間休む、といった使い方もできます。
普段はメニューバー(macOS)か通知領域(Windows)にアイコンがあるだけで、メインウィンドウはありません。トレイのメニューから「今すぐ休憩」「30 分間停止」「休憩時間の変更」ができ、次の休憩時刻はアイコンのツールチップで確認できます。
使い方
インストール
Releases から OS に合ったファイルを取得します。
| OS | ファイル |
|---|---|
| macOS(Apple silicon) | hitoiki-<version>-arm64.dmg |
| Windows | hitoiki.Setup.<version>.exe |
個人で作っているアプリなので、Apple の Developer ID や Windows のコード署名証明書では署名していません。初回起動時に OS の確認画面が出ます。
- macOS: 一度開いて「完了」を押したあと、システム設定 → プライバシーとセキュリティ の「このまま開く」から起動します。
- Windows: SmartScreen の画面で 詳細情報 → 実行 を押します。
気になる方は、リポジトリを clone して npm ci && npm run dist で自分でビルドすることもできます(Node.js 22 が必要)。Intel Mac 向けの配布物は用意していないので、その場合も自分でビルドしてください。
設定
設定画面はなく、JSON ファイルを直接編集します。トレイの「設定ファイルを開く」で開き、編集したら「設定を再読込」で反映します。
{
"intervalMinutes": 50,
"clockTimes": ["15:00", "17:30", { "time": "12:00", "breakSeconds": 3600 }],
"flashCount": 3,
"flashIntervalMs": 400,
"flashOpacity": 0.85,
"flashColor": "#FFFFFF",
"fadeMode": false,
"breakSeconds": 180
}
主な項目は次のとおりです。
| キー | 既定値 | 内容 |
|---|---|---|
intervalMinutes |
50 |
N 分ごとに休憩する。0 で無効 |
clockTimes |
[] |
休憩する時刻。{ "time", "breakSeconds" } の形で長さも個別に指定できる |
breakSeconds |
180 |
休憩の長さ(秒)の既定値 |
flashColor / flashOpacity
|
#FFFFFF / 0.85
|
フラッシュの色と最大の不透明度 |
fadeMode |
false |
true にすると、ふわっと明滅する |
白いフラッシュがまぶしすぎる場合は、flashOpacity を下げるか fadeMode を true にすると穏やかになります。範囲外の値は近い境界値に丸め、JSON として読めないときは既定値で動きます。設定に問題があるとトレイのメニューに警告が出ます。
実装で気を配ったところ
Hitoiki は Electron + TypeScript で書いています。依存は electron、electron-builder、typescript、vitest の 4 つだけで、バンドラーも使っていません。
点滅は 1 秒に 3 回までに抑える
画面全体を光らせる以上、光過敏への配慮は外せません。点滅の周期には 334ms の下限を設け、1 秒に 3 回を超えて点滅しないようにしています。設定ファイルに "flashIntervalMs": 100 と書いても、334ms に丸められます。
光の点滅で体調を崩しやすい方は、flashOpacity を低くし、fadeMode を true にしてお使いください。
全モニターを、フルスクリーンアプリの上からも覆う
オーバーレイは、screen.getAllDisplays() で取得したディスプレイごとに BrowserWindow を 1 枚ずつ作っています。ウィンドウの作り方は次のとおりです(抜粋)。
const win = new BrowserWindow({
...bounds,
show: false,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
resizable: false,
// Otherwise macOS may push the window below the menu bar and leave the top edge uncovered.
enableLargerThanScreen: true,
webPreferences: {
preload: PRELOAD_JS,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
win.setAlwaysOnTop(true, 'screen-saver');
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
// Match the display exactly instead of setFullScreen(true), which breaks transparency on Windows.
win.setBounds(bounds);
macOS では enableLargerThanScreen: true を付けないと、ウィンドウがメニューバーの下へずらされて上端が覆えません。Windows では setFullScreen(true) が透明ウィンドウとうまく噛み合わないので、ディスプレイの bounds に合わせるだけにしました。'screen-saver' レベルの alwaysOnTop と visibleOnFullScreen を組み合わせると、macOS で他のアプリがフルスクリーンになっていてもその上に出ます。
休憩中にモニターを抜き差ししても、ウィンドウを作ったり消したり、位置を合わせ直したりして付いていきます。
OS の入力フックは使わない
作業を確実に止めたいなら、OS レベルの入力フックで全入力を奪う方法もあります。ただ、それをやると Hitoiki 自体に不具合があったときに PC が操作できなくなるおそれがあります。そこで入力フックは使わず、Electron の通常の API の範囲で次のことをしています。
- 休憩中にオーバーレイからフォーカスが外れたら(Cmd+Tab / Alt+Tab など)、少し待ってから取り返す
- Alt+F4 や Cmd+W での close を
preventDefaultで拒否する - アプリケーションメニューを
Menu.setApplicationMenu(null)で無くし、Cmd+Q で抜けられないようにする
フォーカスを取り返す処理は次のようになっています。
private refocusIfLost(): void {
if (this.current !== 'breaking') return;
setTimeout(() => {
if (this.current !== 'breaking') return;
if (this.liveWindows().some((win) => win.isFocused())) return;
this.focusOverlay();
}, REFOCUS_DELAY_MS);
}
穴もあって、アプリを切り替えた直後の一瞬のキー入力は、フォーカスを取り返す前に他のアプリへ届くことがあります。それでも構わないと考えています。休憩を無視するのにひと手間かかれば、目的は果たせるからです。
一方で、ログオフやシャットダウンまで止めてしまうと困ります。Windows のログオフ時には before-quit が来ないので、ウィンドウの query-session-end / session-end でオーバーレイを片付けるようにしています。
待機中は何もしない
常駐アプリなので、休憩の合間に CPU やメモリを食わないようにしています。
- タイマーは
setTimeoutを 1 本だけ張り、「次の休憩までのミリ秒」だけ待つ。ポーリングはしない - 待機中はオーバーレイ用の
BrowserWindowを作らない。レンダラープロセスも存在しない - ネットワーク通信はしない
スリープから復帰したときは powerMonitor の resume でタイマーを張り直します。タイマーが切れた時点でまだ予定時刻より前なら(OS の時計を戻したときなど)、発火せずに次回を計算し直します。
スケジューラは純粋関数にしてテストする
「次にいつ休憩するか」の計算は、Electron に依存しない純粋関数にまとめました。現在時刻は now: () => Date として外から渡すので、vitest で時刻を自由に動かしてテストできます。
export function nextFireTime(
after: Date,
anchor: Date,
config: ScheduleConfig,
lastFiredAt: Date | null = null,
): Fire | null {
// 本体は省略。interval と clock の候補を集めていちばん早い時刻を返し、
// 同じ分に重なった候補は 1 回にまとめて clock 側の休憩の長さを使う。
}
細かいルールはテストで固めています。間隔と時刻の休憩が同じ分に重なったら 1 回にまとめる。休憩中に来た次の休憩は捨てる。夏時間の切り替え日に 2 回ある時刻や、存在しない時刻もきちんと扱う。日本に夏時間はありませんが、タイムゾーンを海外に設定している人もいるので対応しておきました。
おわりに
もともとは自分のためだけに作ったアプリです。公開したのは、ASD や ADHD に限らず、PC 作業に没頭して休憩を忘れてしまう人は少なくないと思ったからです。通知では止まれなかったという方がいたら、試してみてください。休憩の間隔や長さ、フラッシュの色は設定ファイルで自分に合わせて変えられます。
不具合や要望は GitHub の Issue で受け付けています。ライセンスは MIT です。

