ストレスメーター
設定難易度

毎日のイライラ、なんとなく溜め込んでいませんか?

「ストレスメーター」は、日々のストレスを0〜100%で表示するScriptableウィジェットです。

何もしなくても少しずつストレスが上昇。
ムカッとした日は怒りレベルを記録すると一気に上昇し、休憩や気晴らしをするとストレスが減少します。

さらに、今のペースが続いた場合の「爆発予定日」まで表示します(笑

もちろん医学的なストレス測定ではありません。
毎日のイライラをちょっと笑って記録するための、ジョーク系ウィジェットです。

ウィジェットの表示内容

怒りや気晴らしの操作を自分で記録し、コード内のルールで数値を増減します。

サイズごとの表示

  • 小:現在のストレス値とレベルなどを表示します。
  • 中:今日の最大値やダメージ・平和度なども確認できます。
  • 大:怒りの記録やアドバイスを含めて表示します。

設定手順

上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、ストレスメーターなどの名前で保存してください。

    保存したらこのページに戻り、次の手順へ進みます。

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: red; icon-glyph: fire;
    // Version: 1.0.0
    
    // =====================================================
    // 😤 ストレスメーター - Scriptable Widget V1.0.0
    // =====================================================
    //
    // Parameter:
    //   自然に増えるストレス量(1時間あたり)
    //
    // 例:
    //   0.1 → 1時間 +0.1%(1日 +2.4%)
    //   0.2 → 1時間 +0.2%(1日 +4.8%)
    //   0.5 → 1時間 +0.5%(1日 +12%)
    //
    // 未入力 → 0.1
    //
    // -----------------------------------------------------
    //
    // ・普段は時間とともにじわじわ上昇
    // ・タップ → 怒りを5段階で記録
    // ・休憩 / 気晴らしでストレス減少
    // ・🔥 本日の最高ストレス
    // ・💥 本日の被ダメージ
    // ・🕊 平和記録
    // ・🌋 爆発予定日時
    //
    // APIキー不要
    // サーバー不要
    // Keychainへ端末内保存
    //
    // ※ストレス値・Lv・称号などは医学的な測定ではなく、
    //   ジョーク表現です。
    // =====================================================
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const CONFIG = {
    
      KEY: "geboku_stress_meter_state",
    
      // デフォルト自然上昇量(1時間)
      RATE_PER_HOUR: 0.1,
    
      // 初回起動時
      INITIAL: 10,
    
      // 怒り
      ANGER: [
    	{ label: "😒 ムカッ",   key: "muka", add: 5  },
    	{ label: "😠 イラッ",   key: "ira",  add: 10 },
    	{ label: "😡 激おこ",   key: "geki", add: 20 },
    	{ label: "🤬 ブチ切れ", key: "kire", add: 40 },
    	{ label: "💥 もう無理", key: "muri", add: 60 }
      ],
    
      // 回復
      REST: [
    	{ label: "☕ ちょっと休憩", minus: 10 },
    	{ label: "🌿 気晴らしした", minus: 25 },
    	{ label: "🛁 風呂&爆睡",   minus: 50 }
      ],
    
      // Lv
      // 100%だけMAX
      LEVELS: [
    	{ max: 16,  level: "0",   title: "穏やかな人",       color: "#4CD964" },
    	{ max: 31,  level: "1",   title: "ちょっとモヤモヤ", color: "#A8E05F" },
    	{ max: 51,  level: "2",   title: "ちょっと限界",     color: "#FFD60A" },
    	{ max: 71,  level: "3",   title: "そろそろ危険",     color: "#FF9F0A" },
    	{ max: 90,  level: "4",   title: "噴火直前",         color: "#FF453A" },
    	{ max: 100, level: "5",   title: "限界突破",         color: "#FF375F" },
    	{ max: 101, level: "MAX", title: "爆発済み 💥",      color: "#BF5AF2" }
      ],
    
      ADVICE: [
    	"今日も平和。その調子。",
    	"温かい飲み物でも飲もう。",
    	"深呼吸して、5分だけ外の空気を。",
    	"今日は予定を減らそう。甘いものもOK。",
    	"今すぐ距離を取ろう。風呂に入ってさっぱりしよう。",
    	"今日はもう頑張らなくていい。",
    	"もう何もしなくていい日。休もう。"
      ],
    
      COMMENTS: [
    	[
    	  "心が凪いでいます。",
    	  "今日は機嫌がいい日。",
    	  "この穏やかさを覚えておこう。",
    	  "ストレスほぼゼロ。平和。"
    	],
    	[
    	  "ちょっとだけモヤっとしてる。",
    	  "まだ余裕。でも水は飲もう。",
    	  "小さなイライラが溜まり始めた。",
    	  "好きな音楽でも聴こうか。"
    	],
    	[
    	  "そろそろ限界が見えてきた。",
    	  "無理してない? 深呼吸しよう。",
    	  "頑張りすぎのサインかも。",
    	  "ここで休むのが一番コスパいい。"
    	],
    	[
    	  "だいぶ溜まってきてます。",
    	  "人に当たる前に一旦離れよう。",
    	  "今日はもう十分がんばった。",
    	  "予定を1個減らしてもいい。"
    	],
    	[
    	  "噴火直前。冷静な判断は明日に。",
    	  "今は何も決めなくていい。",
    	  "スマホを置いて少し目を閉じよう。",
    	  "今日はもう休む日にしよう。"
    	],
    	[
    	  "かなり危険です。",
    	  "爆発まであと少し。",
    	  "もう今日は店じまいでいい。",
    	  "これ以上は明日の自分に任せよう。"
    	],
    	[
    	  "爆発しました。",
    	  "本日の営業は終了しました。",
    	  "もう何もしなくていい。",
    	  "ここまでよく耐えました。"
    	]
      ],
    
      ANGER_COMMENTS: [
    	"それは相手が悪い。",
    	"はい、今日はもう帰ろう。",
    	"その件は明日の自分に任せよう。",
    	"知らん。風呂入ろ。",
    	"とりあえず甘いもの食べよ。",
    	"今日はもう十分やった。",
    	"一旦スマホ置こ。",
    	"その話、今日は忘れよう。"
      ]
    };
    
    
    // =====================================================
    // Parameter
    // =====================================================
    
    const PARAMETER_RATE = parseRate(args.widgetParameter);
    
    function parseRate(value) {
    
      if (value === null || value === undefined) {
    	return CONFIG.RATE_PER_HOUR;
      }
    
      const str = String(value).trim();
    
      if (!str) {
    	return CONFIG.RATE_PER_HOUR;
      }
    
      const n = Number(str);
    
      if (!Number.isFinite(n) || n < 0) {
    	return CONFIG.RATE_PER_HOUR;
      }
    
      return Math.min(n, 10);
    }
    
    
    // =====================================================
    // カラー
    // =====================================================
    
    const COLORS = {
      bg:    new Color("#111318"),
      white: new Color("#FFFFFF"),
      sub:   new Color("#AEB4BF"),
      muted: new Color("#727985"),
      track: new Color("#30343C")
    };
    
    
    // =====================================================
    // 実行
    // =====================================================
    
    const state = loadState();
    
    prepareToday(state);
    
    const nowMs = Date.now();
    const stress = currentStress(state, nowMs);
    
    updateTodayMax(state, stress);
    saveState(state);
    
    if (!config.runsInWidget) {
    
      await showMenu();
    
    } else {
    
      const widget = createWidget();
      Script.setWidget(widget);
    }
    
    Script.complete();
    
    
    // =====================================================
    // 日付
    // =====================================================
    
    function dateKey(date = new Date()) {
    
      return [
    	date.getFullYear(),
    	String(date.getMonth() + 1).padStart(2, "0"),
    	String(date.getDate()).padStart(2, "0")
      ].join("-");
    }
    
    
    function todayKey() {
      return dateKey();
    }
    
    
    function yesterdayKey() {
    
      const d = new Date();
      d.setDate(d.getDate() - 1);
    
      return dateKey(d);
    }
    
    
    // =====================================================
    // 初期データ
    // =====================================================
    
    function emptyLog() {
    
      return {
    	date: todayKey(),
    	muka: 0,
    	ira: 0,
    	geki: 0,
    	kire: 0,
    	muri: 0
      };
    }
    
    
    function emptyDaily() {
    
      return {
    	date: todayKey(),
    	maxStress: CONFIG.INITIAL,
    	damage: 0
      };
    }
    
    
    // =====================================================
    // 保存・読み込み
    // =====================================================
    
    function loadState() {
    
      if (Keychain.contains(CONFIG.KEY)) {
    
    	try {
    
    	  const s = JSON.parse(
    		Keychain.get(CONFIG.KEY)
    	  );
    
    	  if (
    		Number.isFinite(s.stress) &&
    		Number.isFinite(s.updatedAt)
    	  ) {
    
    		if (!s.log) {
    		  s.log = emptyLog();
    		}
    
    		// 旧バージョン互換
    		if (!Number.isFinite(s.log.muka)) s.log.muka = 0;
    		if (!Number.isFinite(s.log.ira))  s.log.ira = 0;
    		if (!Number.isFinite(s.log.geki)) s.log.geki = 0;
    		if (!Number.isFinite(s.log.kire)) s.log.kire = 0;
    		if (!Number.isFinite(s.log.muri)) s.log.muri = 0;
    
    		if (!Number.isFinite(s.lastRest)) {
    		  s.lastRest = 0;
    		}
    
    		if (!s.daily) {
    		  s.daily = emptyDaily();
    		}
    
    		if (!Number.isFinite(s.daily.maxStress)) {
    		  s.daily.maxStress = s.stress;
    		}
    
    		if (!Number.isFinite(s.daily.damage)) {
    		  s.daily.damage = 0;
    		}
    
    		if (!Number.isFinite(s.peaceStreak)) {
    		  s.peaceStreak = 0;
    		}
    
    		if (!s.peaceDate) {
    		  s.peaceDate = "";
    		}
    
    		return s;
    	  }
    
    	} catch (e) {}
      }
    
    
      const init = {
    
    	stress: CONFIG.INITIAL,
    	updatedAt: Date.now(),
    
    	lastRest: 0,
    
    	log: emptyLog(),
    	daily: emptyDaily(),
    
    	peaceStreak: 0,
    	peaceDate: ""
      };
    
    
      Keychain.set(
    	CONFIG.KEY,
    	JSON.stringify(init)
      );
    
      return init;
    }
    
    
    function saveState(s) {
    
      Keychain.set(
    	CONFIG.KEY,
    	JSON.stringify(s)
      );
    }
    
    
    // =====================================================
    // 日付変更処理
    // =====================================================
    
    function prepareToday(s) {
    
      const today = todayKey();
    
      if (!s.log || s.log.date !== today) {
    
    	const oldLog = s.log;
    
    	if (
    	  oldLog &&
    	  oldLog.date === yesterdayKey()
    	) {
    
    	  if (!hadExplosionEvent(oldLog)) {
    
    		if (s.peaceDate !== oldLog.date) {
    
    		  s.peaceStreak =
    			Number.isFinite(s.peaceStreak)
    			  ? s.peaceStreak + 1
    			  : 1;
    
    		  s.peaceDate = oldLog.date;
    		}
    
    	  } else {
    
    		s.peaceStreak = 0;
    		s.peaceDate = oldLog.date;
    	  }
    	}
    
    	s.log = emptyLog();
      }
    
    
      if (
    	!s.daily ||
    	s.daily.date !== today
      ) {
    
    	s.daily = {
    	  date: today,
    	  maxStress: currentStress(s, Date.now()),
    	  damage: 0
    	};
      }
    
    
      saveState(s);
    }
    
    
    // =====================================================
    // ストレス計算
    // =====================================================
    
    function currentStress(s, ms) {
    
      const hours = Math.max(
    	0,
    	(ms - s.updatedAt) / 3600000
      );
    
      return clamp(
    	s.stress + hours * PARAMETER_RATE,
    	0,
    	100
      );
    }
    
    
    function applyDelta(delta) {
    
      const now = Date.now();
    
      state.stress = clamp(
    	currentStress(state, now) + delta,
    	0,
    	100
      );
    
      state.updatedAt = now;
    
      updateTodayMax(
    	state,
    	state.stress
      );
    }
    
    
    function updateTodayMax(s, value) {
    
      if (!s.daily) {
    	s.daily = emptyDaily();
      }
    
      s.daily.maxStress = Math.max(
    	Number(s.daily.maxStress) || 0,
    	value
      );
    }
    
    
    // =====================================================
    // タップ時メニュー
    // =====================================================
    
    async function showMenu() {
    
      const current = currentStress(
    	state,
    	Date.now()
      );
    
      const main = new Alert();
    
      main.title = "🤯 ストレスメーター";
    
      main.message =
    	`現在 ${Math.round(current)}%  (${levelText(getLevel(current))})\n` +
    	getAdvice(current);
    
      main.addAction("💢 腹が立った");
      main.addAction("🌿 休んだ / 気晴らしした");
      main.addAction("📒 今日の記録を見る");
      main.addCancelAction("閉じる");
    
      const r = await main.presentSheet();
    
    
      // ===================================================
      // 怒り
      // ===================================================
    
      if (r === 0) {
    
    	const a = new Alert();
    
    	a.title = "どのくらい腹が立った?";
    
    	for (const item of CONFIG.ANGER) {
    
    	  a.addAction(
    		`${item.label}  (+${item.add}%)`
    	  );
    	}
    
    	a.addCancelAction("やっぱりやめる");
    
    	const i = await a.presentSheet();
    
    	if (i < 0) return;
    
    	const item = CONFIG.ANGER[i];
    
    	applyDelta(item.add);
    
    	state.log[item.key] += 1;
    	state.daily.damage += item.add;
    
    	// ブチ切れ / もう無理 → 平和記録終了
    	if (
    	  item.key === "kire" ||
    	  item.key === "muri"
    	) {
    	  state.peaceStreak = 0;
    	}
    
    	saveState(state);
    
    
    	const done = new Alert();
    
    	if (state.stress >= 100) {
    
    	  done.title = "💥 爆発しました";
    
    	  done.message =
    		`ストレス 100%\n\n` +
    		"本日の営業は終了しました。\n\n" +
    		getAdvice(state.stress);
    
    	} else {
    
    	  done.title = "記録しました";
    
    	  done.message =
    		`ストレス ${Math.round(state.stress)}%\n\n` +
    		`${randomAngerComment()}\n\n` +
    		getAdvice(state.stress);
    	}
    
    	done.addAction("OK");
    
    	await done.presentAlert();
      }
    
    
      // ===================================================
      // 休憩
      // ===================================================
    
      if (r === 1) {
    
    	const a = new Alert();
    
    	a.title = "何をした?";
    
    	for (const item of CONFIG.REST) {
    
    	  a.addAction(
    		`${item.label}  (-${item.minus}%)`
    	  );
    	}
    
    	a.addCancelAction("やっぱりやめる");
    
    	const i = await a.presentSheet();
    
    	if (i < 0) return;
    
    	applyDelta(
    	  -CONFIG.REST[i].minus
    	);
    
    	state.lastRest = Date.now();
    
    	saveState(state);
    
    
    	const done = new Alert();
    
    	done.title = "えらい!";
    
    	done.message =
    	  `ストレス ${Math.round(state.stress)}%\n\n` +
    	  "休むのも立派な仕事です。";
    
    	done.addAction("OK");
    
    	await done.presentAlert();
      }
    
    
      // ===================================================
      // 記録
      // ===================================================
    
      if (r === 2) {
    
    	const current = currentStress(
    	  state,
    	  Date.now()
    	);
    
    	const info = new Alert();
    
    	info.title = "📒 今日の記録";
    
    	info.message =
    	  `現在のストレス\n${Math.round(current)}%\n\n` +
    
    	  `🔥 本日最高\n${Math.round(state.daily.maxStress)}%\n\n` +
    
    	  `💥 被ダメージ\n+${state.daily.damage}\n\n` +
    
    	  `今日の怒り\n${logText()}\n\n` +
    
    	  `🕊 平和記録\n${peaceText()}\n\n` +
    
    	  `最後に休んだのは\n${lastRestText()}\n\n` +
    
    	  `🌋 爆発予定\n${explosionText()}`;
    
    	info.addAction("OK");
    
    	await info.presentAlert();
      }
    }
    
    
    // =====================================================
    // ウィジェット
    // =====================================================
    
    function createWidget() {
    
      const widget = new ListWidget();
    
      widget.backgroundColor = COLORS.bg;
    
      widget.url =
    	URLScheme.forRunningScript();
    
      widget.refreshAfterDate =
    	new Date(
    	  Date.now() +
    	  15 * 60 * 1000
    	);
    
      const family =
    	config.widgetFamily || "small";
    
    
      if (family === "small") {
    
    	buildSmall(widget);
    
      } else if (family === "medium") {
    
    	buildMedium(widget);
    
      } else {
    
    	buildLarge(widget);
      }
    
    
      return widget;
    }
    
    
    // =====================================================
    // SMALL
    // =====================================================
    
    function buildSmall(widget) {
    
      widget.setPadding(
    	14, 14, 13, 14
      );
    
      const lv = getLevel(stress);
      const color = new Color(lv.color);
    
      addText(
    	widget,
    	"🤯 ストレスメーター",
    	12,
    	true,
    	COLORS.white
      );
    
      widget.addSpacer(8);
    
      addText(
    	widget,
    	`Lv.${lv.level}`,
    	24,
    	true,
    	color
      );
    
      widget.addSpacer(5);
    
      addProgressBar(
    	widget,
    	stress / 100,
    	color,
    	130
      );
    
      widget.addSpacer(4);
    
      addText(
    	widget,
    	`${Math.round(stress)} / 100`,
    	11,
    	true,
    	COLORS.sub
      );
    
      widget.addSpacer(5);
    
      addText(
    	widget,
    	lv.title,
    	13,
    	true,
    	COLORS.white
      );
    
      widget.addSpacer(3);
    
      const adv = addText(
    	widget,
    	getAdvice(stress),
    	10,
    	false,
    	COLORS.sub
      );
    
      adv.lineLimit = 2;
    }
    
    
    // =====================================================
    // MEDIUM
    // =====================================================
    
    function buildMedium(widget) {
    
      widget.setPadding(
    	14, 16, 13, 16
      );
    
      const lv = getLevel(stress);
      const color = new Color(lv.color);
    
      addText(
    	widget,
    	"🤯 ストレスメーター",
    	12,
    	true,
    	COLORS.white
      );
    
      widget.addSpacer(8);
    
      const row = widget.addStack();
      row.layoutHorizontally();
    
    
      // LEFT
      const left = row.addStack();
      left.layoutVertically();
    
      addText(
    	left,
    	`Lv.${lv.level}  ${Math.round(stress)}%`,
    	22,
    	true,
    	color
      );
    
      left.addSpacer(6);
    
      addProgressBar(
    	left,
    	stress / 100,
    	color,
    	130
      );
    
      left.addSpacer(7);
    
      addText(
    	left,
    	lv.title,
    	12,
    	true,
    	COLORS.white
      );
    
      left.addSpacer(3);
    
      const adv = addText(
    	left,
    	getAdvice(stress),
    	10,
    	false,
    	COLORS.sub
      );
    
      adv.lineLimit = 2;
    
    
      row.addSpacer();
    
    
      // RIGHT
      const right = row.addStack();
      right.layoutVertically();
    
      addText(
    	right,
    	"🔥 本日最高",
    	9,
    	false,
    	COLORS.sub
      );
    
      right.addSpacer(1);
    
      addText(
    	right,
    	`${Math.round(state.daily.maxStress)}%`,
    	12,
    	true,
    	COLORS.white
      );
    
      right.addSpacer(6);
    
      addText(
    	right,
    	"💥 被ダメージ",
    	9,
    	false,
    	COLORS.sub
      );
    
      right.addSpacer(1);
    
      addText(
    	right,
    	`+${state.daily.damage}`,
    	12,
    	true,
    	COLORS.white
      );
    
      right.addSpacer(6);
    
      addText(
    	right,
    	"🌋 爆発予定",
    	9,
    	false,
    	COLORS.sub
      );
    
      right.addSpacer(1);
    
      addText(
    	right,
    	explosionShort(),
    	12,
    	true,
    	COLORS.white
      );
    }
    
    
    // =====================================================
    // LARGE
    // =====================================================
    
    function buildLarge(widget) {
    
      widget.setPadding(
    	16, 17, 15, 17
      );
    
      const lv = getLevel(stress);
      const color = new Color(lv.color);
    
      addText(
    	widget,
    	"🤯 ストレスメーター",
    	13,
    	true,
    	COLORS.white
      );
    
      widget.addSpacer(8);
    
    
      // LEVEL
      addText(
    	widget,
    	`Lv.${lv.level}  ${lv.title}`,
    	24,
    	true,
    	color
      );
    
      widget.addSpacer(6);
    
    
      // PROGRESS
      addProgressBar(
    	widget,
    	stress / 100,
    	color,
    	210
      );
    
      widget.addSpacer(4);
    
    
      // VALUE + 爆発予定
      const expRow = widget.addStack();
      expRow.layoutHorizontally();
    
      addText(
    	expRow,
    	`${Math.round(stress)} / 100`,
    	10,
    	true,
    	color
      );
    
      expRow.addSpacer();
    
      addText(
    	expRow,
    	`🌋 爆発予定 ${explosionShort()}`,
    	10,
    	false,
    	COLORS.sub
      );
    
    
      widget.addSpacer(11);
    
    
      // ===================================================
      // 今日の怒りログ
      // ===================================================
    
      addText(
    	widget,
    	"今日の怒りログ",
    	11,
    	true,
    	COLORS.sub
      );
    
      widget.addSpacer(4);
    
      const log = getTodayLog();
    
    
      // ------------------------------
      // 1段目
      // ムカッ | イラッ | 激おこ
      // ------------------------------
    
      const logRow1 = widget.addStack();
      logRow1.layoutHorizontally();
    
      addLogGridCell(
    	logRow1,
    	"😒 ムカッ",
    	log.muka,
    	"left"
      );
    
      addLogGridCell(
    	logRow1,
    	"😠 イラッ",
    	log.ira,
    	"center"
      );
    
      addLogGridCell(
    	logRow1,
    	"😡 激おこ",
    	log.geki,
    	"right"
      );
    
    
      widget.addSpacer(6);
    
    
      // ------------------------------
      // 2段目
      // ブチ切れ | 空白 | もう無理
      // ------------------------------
    
      const logRow2 = widget.addStack();
      logRow2.layoutHorizontally();
    
      addLogGridCell(
    	logRow2,
    	"🤬 ブチ切れ",
    	log.kire,
    	"left"
      );
    
      addEmptyLogGridCell(
    	logRow2
      );
    
      addLogGridCell(
    	logRow2,
    	"💥 もう無理",
    	log.muri,
    	"right"
      );
    
    
      widget.addSpacer(10);
    
    
      // ===================================================
      // 今日の統計
      // ===================================================
    
      const stats = widget.addStack();
      stats.layoutHorizontally();
    
      addStatBlock(
    	stats,
    	"🔥 本日最高",
    	`${Math.round(state.daily.maxStress)}%`
      );
    
      stats.addSpacer();
    
      addStatBlock(
    	stats,
    	"💥 被ダメージ",
    	`+${state.daily.damage}`
      );
    
      stats.addSpacer();
    
      addStatBlock(
    	stats,
    	"🕊 平和記録",
    	peaceText()
      );
    
    
      widget.addSpacer(10);
    
    
      // ===================================================
      // 最後に休んだ
      // ===================================================
    
      addText(
    	widget,
    	"最後に休んだ",
    	10,
    	false,
    	COLORS.sub
      );
    
      widget.addSpacer(2);
    
      addText(
    	widget,
    	lastRestText(),
    	14,
    	true,
    	COLORS.white
      );
    
    
      widget.addSpacer(9);
    
    
      // ===================================================
      // おすすめ
      // ===================================================
    
      addText(
    	widget,
    	"💡 おすすめ",
    	11,
    	true,
    	COLORS.sub
      );
    
      widget.addSpacer(3);
    
      const adv = addText(
    	widget,
    	getAdvice(stress),
    	13,
    	true,
    	COLORS.white
      );
    
      adv.lineLimit = 2;
    
      widget.addSpacer(7);
    
      const comment = addText(
    	widget,
    	`「${getComment(stress)}」`,
    	11,
    	false,
    	COLORS.sub
      );
    
      comment.lineLimit = 2;
    
      widget.addSpacer();
    
      addText(
    	widget,
    	"※ジョーク表現です。つらい時は無理せず休んでね",
    	9,
    	false,
    	COLORS.muted
      );
    }
    
    
    // =====================================================
    // 怒りログ 3列グリッド
    // =====================================================
    
    function addLogGridCell(
      parent,
      label,
      count,
      align
    ) {
    
      const cell = parent.addStack();
    
      // 3列を同じ幅に固定
      cell.size = new Size(110, 0);
      cell.layoutVertically();
    
    
      // ラベル
      const labelRow = cell.addStack();
      labelRow.layoutHorizontally();
    
      if (
    	align === "center" ||
    	align === "right"
      ) {
    	labelRow.addSpacer();
      }
    
      addText(
    	labelRow,
    	label,
    	10,
    	false,
    	COLORS.sub
      );
    
      if (
    	align === "center" ||
    	align === "left"
      ) {
    	labelRow.addSpacer();
      }
    
    
      cell.addSpacer(2);
    
    
      // 数値
      const valueRow = cell.addStack();
      valueRow.layoutHorizontally();
    
      if (
    	align === "center" ||
    	align === "right"
      ) {
    	valueRow.addSpacer();
      }
    
      addText(
    	valueRow,
    	`× ${count}`,
    	16,
    	true,
    	COLORS.white
      );
    
      if (
    	align === "center" ||
    	align === "left"
      ) {
    	valueRow.addSpacer();
      }
    }
    
    
    // 空白セル
    function addEmptyLogGridCell(parent) {
    
      const cell = parent.addStack();
    
      cell.size = new Size(110, 0);
    }
    
    
    // =====================================================
    // 統計ブロック
    // =====================================================
    
    function addStatBlock(
      parent,
      label,
      value
    ) {
    
      const s = parent.addStack();
    
      s.layoutVertically();
    
      addText(
    	s,
    	label,
    	9,
    	false,
    	COLORS.sub
      );
    
      s.addSpacer(2);
    
      addText(
    	s,
    	value,
    	14,
    	true,
    	COLORS.white
      );
    }
    
    
    // =====================================================
    // LEVEL
    // =====================================================
    
    function getLevelIndex(v) {
    
      for (
    	let i = 0;
    	i < CONFIG.LEVELS.length;
    	i++
      ) {
    
    	if (v < CONFIG.LEVELS[i].max) {
    	  return i;
    	}
      }
    
      return CONFIG.LEVELS.length - 1;
    }
    
    
    function getLevel(v) {
    
      return CONFIG.LEVELS[
    	getLevelIndex(v)
      ];
    }
    
    
    function levelText(lv) {
    
      return `Lv.${lv.level} ${lv.title}`;
    }
    
    
    // =====================================================
    // メッセージ
    // =====================================================
    
    function getAdvice(v) {
    
      return CONFIG.ADVICE[
    	getLevelIndex(v)
      ];
    }
    
    
    function getComment(v) {
    
      const list =
    	CONFIG.COMMENTS[
    	  getLevelIndex(v)
    	];
    
      const seed =
    	Math.floor(
    	  Date.now() / 3600000
    	);
    
      return list[
    	seed % list.length
      ];
    }
    
    
    function randomAngerComment() {
    
      return CONFIG.ANGER_COMMENTS[
    	Math.floor(
    	  Math.random() *
    	  CONFIG.ANGER_COMMENTS.length
    	)
      ];
    }
    
    
    // =====================================================
    // LOG
    // =====================================================
    
    function getTodayLog() {
    
      return state.log.date === todayKey()
    	? state.log
    	: emptyLog();
    }
    
    
    function totalLog(log) {
    
      return (
    	log.muka +
    	log.ira +
    	log.geki +
    	log.kire +
    	log.muri
      );
    }
    
    
    function hadExplosionEvent(log) {
    
      return (
    	(log.kire || 0) > 0 ||
    	(log.muri || 0) > 0
      );
    }
    
    
    function logText() {
    
      const l = getTodayLog();
    
      if (totalLog(l) === 0) {
    	return "被害なし。平和な一日。";
      }
    
      return (
    	`ムカッ×${l.muka}\n` +
    	`イラッ×${l.ira}\n` +
    	`激おこ×${l.geki}\n` +
    	`ブチ切れ×${l.kire}\n` +
    	`もう無理×${l.muri}`
      );
    }
    
    
    // =====================================================
    // 平和記録
    // =====================================================
    
    function peaceText() {
    
      const log = getTodayLog();
    
      if (hadExplosionEvent(log)) {
    	return "0日";
      }
    
      return `${state.peaceStreak}日`;
    }
    
    
    // =====================================================
    // 最後に休んだ
    // =====================================================
    
    function lastRestText() {
    
      if (!state.lastRest) {
    	return "まだ記録なし";
      }
    
      const ms = Math.max(
    	0,
    	Date.now() - state.lastRest
      );
    
      return formatDuration(ms) + "前";
    }
    
    
    // =====================================================
    // 爆発予報
    // =====================================================
    
    function explosionDate() {
    
      if (stress >= 100) {
    	return null;
      }
    
      if (PARAMETER_RATE <= 0) {
    	return null;
      }
    
      const hours =
    	(100 - stress) /
    	PARAMETER_RATE;
    
      return new Date(
    	Date.now() +
    	hours * 3600000
      );
    }
    
    
    function explosionShort() {
    
      if (stress >= 100) {
    	return "爆発済み 💥";
      }
    
      if (PARAMETER_RATE <= 0) {
    	return "自然上昇なし";
      }
    
      const d = explosionDate();
    
      if (!d) {
    	return "予定なし";
      }
    
      const now = new Date();
    
      const tomorrow = new Date();
    
      tomorrow.setDate(
    	tomorrow.getDate() + 1
      );
    
      const time =
    	`${String(d.getHours()).padStart(2, "0")}:` +
    	`${String(d.getMinutes()).padStart(2, "0")}`;
    
      if (sameDate(d, now)) {
    	return `今日 ${time}`;
      }
    
      if (sameDate(d, tomorrow)) {
    	return `明日 ${time}`;
      }
    
      return (
    	`${d.getMonth() + 1}/${d.getDate()} ${time}`
      );
    }
    
    
    function explosionText() {
    
      if (stress >= 100) {
    	return "すでに爆発しています 💥";
      }
    
      if (PARAMETER_RATE <= 0) {
    	return "自然上昇なし";
      }
    
      return (
    	`${explosionShort()}\n` +
    	`(自然上昇 +${formatRate(PARAMETER_RATE)}% / 時間)`
      );
    }
    
    
    function formatRate(value) {
    
      return String(
    	Math.round(value * 100) / 100
      );
    }
    
    
    function sameDate(a, b) {
    
      return (
    	a.getFullYear() === b.getFullYear() &&
    	a.getMonth() === b.getMonth() &&
    	a.getDate() === b.getDate()
      );
    }
    
    
    // =====================================================
    // 時間
    // =====================================================
    
    function formatDuration(ms) {
    
      const totalMin =
    	Math.floor(ms / 60000);
    
      const days =
    	Math.floor(totalMin / 1440);
    
      const hours =
    	Math.floor(
    	  (totalMin % 1440) / 60
    	);
    
      const min =
    	totalMin % 60;
    
      if (days > 0) {
    	return `${days}日${hours}時間`;
      }
    
      if (hours > 0) {
    	return `${hours}時間${min}分`;
      }
    
      return `${min}分`;
    }
    
    
    // =====================================================
    // UI部品
    // =====================================================
    
    function addText(
      parent,
      text,
      size,
      bold,
      color
    ) {
    
      const t = parent.addText(text);
    
      t.font = bold
    	? Font.boldSystemFont(size)
    	: Font.systemFont(size);
    
      t.textColor = color;
    
      return t;
    }
    
    
    function addProgressBar(
      parent,
      progress,
      color,
      width
    ) {
    
      const height = 6;
    
      const ctx = new DrawContext();
    
      ctx.size =
    	new Size(
    	  width,
    	  height
    	);
    
      ctx.opaque = false;
      ctx.respectScreenScale = true;
    
    
      // TRACK
      const track = new Path();
    
      track.addRoundedRect(
    	new Rect(
    	  0,
    	  0,
    	  width,
    	  height
    	),
    	height / 2,
    	height / 2
      );
    
      ctx.addPath(track);
    
      ctx.setFillColor(
    	COLORS.track
      );
    
      ctx.fillPath();
    
    
      // VALUE
      const raw =
    	width *
    	clamp(
    	  progress,
    	  0,
    	  1
    	);
    
      if (raw > 0) {
    
    	const w = Math.min(
    	  Math.max(
    		height,
    		raw
    	  ),
    	  width
    	);
    
    	const fill = new Path();
    
    	fill.addRoundedRect(
    	  new Rect(
    		0,
    		0,
    		w,
    		height
    	  ),
    	  height / 2,
    	  height / 2
    	);
    
    	ctx.addPath(fill);
    
    	ctx.setFillColor(color);
    
    	ctx.fillPath();
      }
    
    
      const img =
    	parent.addImage(
    	  ctx.getImage()
    	);
    
      img.imageSize =
    	new Size(
    	  width,
    	  height
    	);
    }
    
    
    // =====================================================
    // UTIL
    // =====================================================
    
    function clamp(
      v,
      min,
      max
    ) {
    
      return Math.min(
    	Math.max(v, min),
    	max
      );
    }
  3. Parameterに設定を入力する

    ウィジェットを長押しして「ウィジェットを編集」を開き、「Parameter」に1時間あたりの上昇率を数字で入力します。例:0.1 なら1時間に0.1%、0.2 なら0.2%増えます。0 で自然上昇を止められます。

  4. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」に上で案内した値を直接入力します。 設定を変えるときは、この欄を書き換えてください。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

  5. 記録方法

    ウィジェットをタップすると、

    • 💢 腹が立った
    • 🌿 休んだ / 気晴らしした
    • 📒 今日の記録を見る

    を選択できます。

    毎日の気分に合わせて記録してください。

  6. 怒りを記録する

    ウィジェットをタップすると、その日の怒りを記録できます。

    状態 ストレス
    😒 ムカッ +5%
    😠 イラッ +10%
    😡 激おこ +20%
    🤬 ブチ切れ +40%
    💥 もう無理 +60%

    「もう無理」を押すと、一気に爆発が近づきます😱

    ※動画ではすぐに反映されていますが、実際のウィジェットは記録内容が反映されるまで少し時間がかかる場合があります。

  7. 休憩・気晴らし

    ちゃんと休めばストレスも減ります。

    行動 ストレス
    ☕ ちょっと休憩 -10%
    🌿 気晴らしした -25%
    🛁 風呂&爆睡 -50%

    怒った記録だけではなく、ちゃんと休んだことも記録できるのがポイントです。

    ※動画ではすぐに反映されていますが、実際のウィジェットは記録内容が反映されるまで少し時間がかかる場合があります。

爆発予定とは?

現在のストレスと自然上昇速度から、

「このまま何も起きなかった場合、いつ100%になるか」

を計算しています。

怒りを記録すると爆発予定が近づき、休憩すると遠ざかります。

未来を予言しているわけではありませんよ(笑

ストレスレベル

ストレス値によって表示されるLvとメッセージが変化します

ストレス レベル
0〜15% Lv.0 穏やかな人
16〜30% Lv.1 ちょっとモヤモヤ
31〜50% Lv.2 ちょっと限界
51〜70% Lv.3 そろそろ危険
71〜89% Lv.4 噴火直前
90〜99% Lv.5 限界突破
100% Lv.MAX 爆発済み 💥

100%に到達すると、ついに爆発です🤯

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.0.0
動作確認日 2026-10-07

表示・使用時のポイント

怒りや気晴らしの操作を自分で記録し、コード内のルールで数値を増減します。自然上昇を0にすると、時間経過による上昇を止められます。

更新履歴

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
タップしても数値が変わらない
開いたメニューで記録や気晴らしの操作を選んでください。
爆発予定が出ない
自然上昇が0なら、時間経過で上限に達する予測は出ません。
100より増えない
値は上限で止まる仕様です。
設定した自然上昇にならない
Parameterには1時間あたりの数値を指定してください。

注意事項・制限

  • このウィジェットで表示されるストレス値、レベル、爆発予定日などは、医学的・心理学的な測定結果ではありません。
  • あくまで日々の気分を楽しく記録するためのジョーク表現です。
  • 実際につらいと感じているときは、ウィジェットの数字に関係なく無理せず休んでください。
  • 数値や爆発予測は遊びのルールによるものです。ストレスの測定や医学的な評価ではありません。
  • 記録は端末側に保存されます。アプリや保存データの削除後は引き継げない場合があります。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

このウィジェットにはデータ提供や参考資料は特にありません。

SHARE

この記事をシェア