最近流行りの Claude はとても優秀で、iPhone アプリも重宝して使っています。
使用量は Claude の管理画面(https://claude.ai/settings/usage)から確認できますが、待受画面やウィジットとして表示できたらカッコいいなという事で Claude に作って貰ったのでシェアします。
Scriptable
iPhone の待受画面やウィジットに追加するために、Scriptable という無料のアプリを使用します。

Scriptableアプリ - App Store
Simon B. Støvringの「Scriptable」をApp Storeでダウンロードしてください。スクリーンショット、評価とレビュー、ユーザのヒント、「Scriptable」に似たゲームを見ることなどができます。
スクリプトの登録
以下のスクリプトを Scriptable に「Claude Usage」として登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 | // ============================================================ // Claude Usage Widget for Scriptable (iOS) // ============================================================ // // 【セットアップ手順】 // // 1. App Store から「Scriptable」をインストール // 2. Safari で claude.ai にログインしておく // 3. このスクリプトを Scriptable に貼り付けて実行 // 4. 自動で Organization ID 検出・データ取得・キャッシュ保存 // // 【ホーム画面ウィジェット】 // ホーム画面長押し > 「+」 > Scriptable > Medium or Small // ウィジェット長押し > 「ウィジェットを編集」> Script を選択 // // 【ロック画面ウィジェット(円グラフ)】 // ロック画面長押し > 「カスタマイズ」> ウィジェット追加 // Scriptable の「円形」または「長方形」を選択 // ウィジェット長押し > 「ウィジェットを編集」> Script を選択 // // 円形ウィジェットの場合、Parameter で表示項目を指定: // session → 現在のセッション(デフォルト) // weekly → すべてのモデル(週間) // sonnet → Sonnetのみ // opus → Opusのみ // 複数配置して、それぞれ異なる Parameter を設定してください。 // // 【データの更新】 // ウィジェットはキャッシュを表示します。 // データを更新するにはウィジェットをタップしてください。 // ショートカット App で定期実行を設定すると自動更新も可能です。 // // ============================================================ // ==================== 定数 ==================== const KEYCHAIN_ORG = "claude_usage_org_id"; const CACHE_FILE = "claude_usage_cache.json"; const REFRESH_MINUTES = 15; const ERROR_REFRESH_MINUTES = 60; const CACHE_FRESH_MINUTES = 30; const CACHE_STALE_MINUTES = 120; // ==================== カラー ==================== const dark = Device.isUsingDarkAppearance(); const C = { bg: dark ? new Color("#111116") : new Color("#f5f5f7"), text: dark ? new Color("#e5e5ea") : new Color("#1c1c1e"), sub: dark ? new Color("#8e8e93") : new Color("#6e6e73"), barBg: dark ? new Color("#2c2c34") : new Color("#e5e5ea"), green: new Color("#30d158"), amber: new Color("#ff9f0a"), red: new Color("#ff453a"), accent: new Color("#d97757"), lockBg: new Color("#1c1c1e", 0), lockFg: new Color("#ffffff"), lockDim: new Color("#ffffff", 0.3), }; // ==================== キャッシュ管理 ==================== const fm = FileManager.local(); const cacheDir = fm.joinPath(fm.documentsDirectory(), "claude-usage"); const cachePath = fm.joinPath(cacheDir, CACHE_FILE); function ensureCacheDir() { if (!fm.fileExists(cacheDir)) { fm.createDirectory(cacheDir, true); } } function writeCache(data) { ensureCacheDir(); const payload = { timestamp: new Date().toISOString(), data: data, }; fm.writeString(cachePath, JSON.stringify(payload)); } function readCache() { if (!fm.fileExists(cachePath)) return null; try { return JSON.parse(fm.readString(cachePath)); } catch { return null; } } // ==================== Keychain(orgId のみ) ==================== function getOrgId() { return Keychain.contains(KEYCHAIN_ORG) ? Keychain.get(KEYCHAIN_ORG) : null; } function saveOrgId(orgId) { Keychain.set(KEYCHAIN_ORG, orgId); } // ==================== WebView 認証済みリクエスト ==================== async function fetchViaWebView(url) { const wv = new WebView(); try { await wv.loadURL(url); } catch (e) { return { ok: false, data: null, err: "loadURL失敗: " + String(e) }; } try { await wv.waitForLoad(); } catch {} let currentURL; try { currentURL = await wv.evaluateJavaScript("window.location.href", false); } catch { currentURL = ""; } if (currentURL.includes("/login") || currentURL.includes("/oauth")) { return { ok: false, data: null, err: "auth" }; } let bodyText; try { bodyText = await wv.evaluateJavaScript( "document.body.innerText || document.body.textContent || ''", false ); } catch (e) { return { ok: false, data: null, err: "JS実行失敗: " + String(e) }; } if (!bodyText || bodyText.trim() === "") { return { ok: false, data: null, err: "空のレスポンス" }; } let json; try { json = JSON.parse(bodyText); } catch { return { ok: false, data: null, err: "parse" }; } if (json && json.error) { const msg = json.error.message || json.error.type || JSON.stringify(json.error); return { ok: false, data: json, err: msg }; } return { ok: true, data: json, err: null }; } // ==================== Organization ID 自動検出 ==================== async function discoverOrgId() { const result = await fetchViaWebView("https://claude.ai/api/organizations"); if (!result.ok) return result; const orgs = result.data; if (!Array.isArray(orgs) || orgs.length === 0) { return { ok: false, data: orgs, err: "組織が見つかりません" }; } const orgId = orgs[0].uuid || orgs[0].id; if (!orgId) { return { ok: false, data: orgs, err: "組織IDを抽出できません" }; } return { ok: true, data: orgId, err: null }; } // ==================== データ取得 ==================== async function fetchUsageLive() { let orgId = getOrgId(); if (!orgId) { const orgResult = await discoverOrgId(); if (!orgResult.ok) { return { ok: false, data: null, err: orgResult.err }; } orgId = orgResult.data; saveOrgId(orgId); } const url = "https://claude.ai/api/organizations/" + orgId + "/usage"; const result = await fetchViaWebView(url); if (!result.ok) { return { ok: false, data: null, err: result.err }; } if (!result.data || result.data.five_hour === undefined) { return { ok: false, data: result.data, err: "unexpected format" }; } writeCache(result.data); return { ok: true, data: result.data, err: null }; } function loadFromCache() { const cache = readCache(); if (!cache || !cache.data) { return { ok: false, data: null, err: "キャッシュなし", timestamp: null }; } return { ok: true, data: cache.data, err: null, timestamp: cache.timestamp }; } function errMessage(err) { if (err === "auth") return "Safari で claude.ai にログインしてから\n再実行してください"; if (err === "parse") return "ログイン切れ、またはサーバーエラーです\nSafari で claude.ai を確認してください"; return err || "不明なエラー"; } function errTitle(err) { if (err === "auth" || err === "parse") return "ログインが必要です"; return "データ取得失敗"; } // ==================== Claude ロゴ ==================== const LOGO_PATH = [ [0,7.75,26.27],[1,15.52,21.91],[1,15.65,21.53],[1,15.52,21.32],[1,15.14,21.32], [1,13.84,21.24],[1,9.40,21.12],[1,5.55,20.96],[1,1.82,20.76],[1,0.88,20.56], [1,-0.00,19.40],[1,0.09,18.82],[1,0.88,18.29],[1,2.01,18.39],[1,4.51,18.56], [1,8.26,18.82],[1,10.98,18.98],[1,15.01,19.40],[1,15.65,19.40],[1,15.74,19.14], [1,15.52,18.98],[1,15.35,18.82],[1,11.47,16.19],[1,7.27,13.41],[1,5.07,11.81], [1,3.88,11.00],[1,3.28,10.24],[1,3.02,8.58],[1,4.10,7.39],[1,5.55,7.49], [1,5.92,7.59],[1,7.39,8.72],[1,10.53,11.15],[1,14.63,14.17],[1,15.23,14.67], [1,15.47,14.50],[1,15.50,14.38],[1,15.23,13.93],[1,13.00,9.90],[1,10.62,5.80], [1,9.56,4.10],[1,9.28,3.08], [2,9.18,2.66,9.11,2.31,9.11,1.88], [1,10.34,0.21],[1,11.02,-0.01],[1,12.66,0.21],[1,13.35,0.81],[1,14.37,3.14], [1,16.02,6.81],[1,18.58,11.80],[1,19.33,13.28],[1,19.73,14.65],[1,19.88,15.07], [1,20.14,15.07],[1,20.14,14.83],[1,20.35,12.02],[1,20.74,8.57],[1,21.12,4.13], [1,21.25,2.88],[1,21.87,1.38],[1,23.10,0.57],[1,24.06,1.03],[1,24.85,2.16], [1,24.74,2.89],[1,24.27,5.94],[1,23.35,10.72],[1,22.75,13.92],[1,23.10,13.92], [1,23.50,13.52],[1,25.12,11.37],[1,27.84,7.97],[1,29.04,6.62],[1,30.44,5.13], [1,31.34,4.42],[1,33.04,4.42],[1,34.29,6.28],[1,33.73,8.20],[1,31.98,10.42], [1,30.53,12.30],[1,28.45,15.10],[1,27.15,17.34],[1,27.27,17.52],[1,27.58,17.49], [1,32.28,16.49],[1,34.82,16.03],[1,37.85,15.51],[1,39.22,16.15],[1,39.37,16.80], [1,38.83,18.13],[1,35.59,18.93],[1,31.79,19.69],[1,26.13,21.03],[1,26.06,21.08], [1,26.14,21.18],[1,28.69,21.42],[1,29.78,21.48],[1,32.45,21.48],[1,37.42,21.85], [1,38.72,22.71],[1,39.50,23.76],[1,39.37,24.56],[1,37.37,25.58],[1,34.67,24.94], [1,28.37,23.44],[1,26.21,22.90],[1,25.91,22.90],[1,25.91,23.08],[1,27.71,24.84], [1,31.01,27.82],[1,35.14,31.66],[1,35.35,32.61],[1,34.82,33.36],[1,34.26,33.28], [1,30.63,30.55],[1,29.23,29.32],[1,26.06,26.65],[1,25.85,26.65],[1,25.85,26.93], [1,26.58,28.00],[1,30.44,33.80],[1,30.64,35.58],[1,30.36,36.16],[1,29.36,36.51], [1,28.26,36.31],[1,26.00,33.14],[1,23.67,29.57],[1,21.79,26.37],[1,21.56,26.50], [1,20.45,38.45],[1,19.93,39.06],[1,18.73,39.52],[1,17.73,38.76],[1,17.20,37.53], [1,17.73,35.10],[1,18.37,31.93],[1,18.89,29.41],[1,19.36,26.28],[1,19.64,25.24], [1,19.62,25.17],[1,19.39,25.20],[1,17.03,28.44],[1,13.44,33.29],[1,10.60,36.33], [1,9.92,36.60],[1,8.74,35.99],[1,8.85,34.90],[1,9.51,33.93],[1,13.44,28.93], [1,15.81,25.83],[1,17.34,24.04],[1,17.33,23.78],[1,17.24,23.78],[1,6.80,30.56], [1,4.94,30.80],[1,4.14,30.05],[1,4.24,28.82],[1,4.62,28.42],[1,7.76,26.26],[3], ]; function drawClaudeLogo(size) { const s = size / 39.53; const ctx = new DrawContext(); ctx.size = new Size(size, size); ctx.opaque = false; ctx.respectScreenScale = true; const p = new Path(); for (const c of LOGO_PATH) { switch (c[0]) { case 0: p.move(new Point(c[1] * s, c[2] * s)); break; case 1: p.addLine(new Point(c[1] * s, c[2] * s)); break; case 2: p.addCurve( new Point(c[5] * s, c[6] * s), new Point(c[1] * s, c[2] * s), new Point(c[3] * s, c[4] * s)); break; case 3: p.closeSubpath(); break; } } ctx.addPath(p); ctx.setFillColor(C.accent); ctx.fillPath(); return ctx.getImage(); } // ==================== ユーティリティ ==================== function barColor(pct) { if (pct >= 80) return C.red; if (pct >= 50) return C.amber; return C.green; } function cacheAgeColor(isoStr) { if (!isoStr) return C.sub; try { const m = (Date.now() - new Date(isoStr).getTime()) / 60000; if (isNaN(m)) return C.sub; if (m <= CACHE_FRESH_MINUTES) return C.green; if (m <= CACHE_STALE_MINUTES) return C.amber; return C.red; } catch { return C.sub; } } function fmtReset(isoStr) { if (!isoStr) return null; try { const reset = new Date(isoStr); if (isNaN(reset.getTime())) return null; const diff = reset - new Date(); if (diff <= 0) return "リセット済み"; const h = Math.floor(diff / 3600000); const m = Math.floor((diff % 3600000) / 60000); if (h >= 24) return `${Math.floor(h / 24)}日 ${h % 24}h`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; } catch { return null; } } function toPct(utilization) { if (utilization === null || utilization === undefined || isNaN(utilization)) { return null; } return Math.min(100, Math.max(0, Math.round(utilization))); } function getBarWidth() { const f = config.widgetFamily; if (f === "small") return 70; if (f === "large") return 200; return 180; } function isSmall() { return config.widgetFamily === "small"; } function isLockScreen() { const f = config.widgetFamily; return f === "accessoryCircular" || f === "accessoryRectangular" || f === "accessoryInline"; } function fmtAge(isoStr) { if (!isoStr) return null; try { const m = Math.floor((Date.now() - new Date(isoStr).getTime()) / 60000); if (isNaN(m)) return null; if (m < 1) return "たった今"; if (m < 60) return `${m}分前`; const h = Math.floor(m / 60); if (h < 24) return `${h}時間前`; return `${Math.floor(h / 24)}日前`; } catch { return null; } } // ==================== データ整形 ==================== // Parameter 文字列から API キーへのマッピング const PARAM_MAP = { session: "five_hour", "5h": "five_hour", weekly: "seven_day", "7d": "seven_day", sonnet: "seven_day_sonnet", opus: "seven_day_opus", oauth: "seven_day_oauth_apps", cowork: "seven_day_cowork", }; // 表示ラベル(短縮版をロック画面用に追加) const DEFS = [ { key: "five_hour", label: "現在のセッション", short: "5h", section: "プラン使用制限" }, { key: "seven_day", label: "すべてのモデル", short: "7d All", section: "週間制限" }, { key: "seven_day_sonnet", label: "Sonnetのみ", short: "Sonnet", section: "週間制限" }, { key: "seven_day_opus", label: "Opusのみ", short: "Opus", section: "週間制限" }, { key: "seven_day_oauth_apps", label: "OAuthアプリ", short: "OAuth", section: "週間制限" }, { key: "seven_day_cowork", label: "Cowork", short: "Cowrk", section: "週間制限" }, ]; function parse(raw) { const items = []; for (const def of DEFS) { const entry = raw[def.key]; if (!entry || entry.utilization === undefined) continue; const pct = toPct(entry.utilization); if (pct === null) continue; items.push({ key: def.key, label: def.label, short: def.short, pct, reset: entry.resets_at || null, section: def.section, }); } const ex = raw.extra_usage; if (ex && ex.is_enabled && ex.utilization !== null && ex.utilization !== undefined) { const pct = toPct(ex.utilization); if (pct !== null) { items.push({ key: "extra", label: "追加使用量", short: "Extra", pct, reset: null, section: "追加使用量" }); } } return items; } /** * Parameter 文字列から対応する項目を1つ返す。 * 見つからなければ最初の項目(セッション)を返す。 */ function getItemByParam(items, param) { if (param) { const key = PARAM_MAP[param.trim().toLowerCase()]; if (key) { const found = items.find((it) => it.key === key); if (found) return found; } } return items[0] || null; } // ==================== 円グラフ描画 ==================== /** * DrawContext で円形プログレスリングを描画する。 * ロック画面ウィジェット用。 * * @param {number} size 画像サイズ * @param {number} pct パーセント (0-100) * @param {string} label 中央下に表示するラベル * @param {number} lineW リングの太さ * @param {Color} fgColor 進捗の色 * @param {Color} bgColor トラックの色 * @param {Color} textColor テキストの色 */ function drawRing(size, pct, lineW, fgColor, bgColor, textColor, logoColor) { const ctx = new DrawContext(); ctx.size = new Size(size, size); ctx.opaque = false; ctx.respectScreenScale = true; const cx = size / 2; const cy = size / 2; const r = (size - lineW) / 2 - 1; const segments = 72; // 背景トラック(フルリング) drawArcPath(ctx, cx, cy, r, 0, 360, lineW, bgColor, segments); // 進捗(12時位置 = -90° から時計回り) if (pct > 0) { const angle = Math.min(pct, 100) / 100 * 360; drawArcPath(ctx, cx, cy, r, -90, -90 + angle, lineW, fgColor, segments); } // 中央: Claude ロゴ(小) const logoSize = Math.floor(size * 0.22); const logoImg = drawClaudeLogoWithColor(logoSize, logoColor); const logoX = cx - logoSize / 2; const logoY = cy - logoSize / 2 - Math.floor(size * 0.13); ctx.drawImageAtPoint(logoImg, new Point(logoX, logoY)); // 中央: パーセント(大) const pctStr = `${pct}%`; const pctFontSize = Math.floor(size * 0.28); ctx.setFont(Font.boldSystemFont(pctFontSize)); ctx.setTextColor(textColor); ctx.setTextAlignedCenter(); const pctRectH = pctFontSize * 1.3; const pctY = cy - pctRectH / 2 + Math.floor(size * 0.10); ctx.drawTextInRect(pctStr, new Rect(0, pctY, size, pctRectH)); return ctx.getImage(); } /** * 任意の色で Claude ロゴを描画する。 * ロック画面では白、ホーム画面ではアクセント色で使い分ける。 */ function drawClaudeLogoWithColor(size, color) { const s = size / 39.53; const ctx = new DrawContext(); ctx.size = new Size(size, size); ctx.opaque = false; ctx.respectScreenScale = true; const p = new Path(); for (const c of LOGO_PATH) { switch (c[0]) { case 0: p.move(new Point(c[1] * s, c[2] * s)); break; case 1: p.addLine(new Point(c[1] * s, c[2] * s)); break; case 2: p.addCurve( new Point(c[5] * s, c[6] * s), new Point(c[1] * s, c[2] * s), new Point(c[3] * s, c[4] * s)); break; case 3: p.closeSubpath(); break; } } ctx.addPath(p); ctx.setFillColor(color); ctx.fillPath(); return ctx.getImage(); } /** * DrawContext に円弧パスを描画する。 * Path + addLine で近似。 */ function drawArcPath(ctx, cx, cy, r, startDeg, endDeg, lineW, color, segments) { const step = (endDeg - startDeg) / segments; // 外側の弧 const outerR = r + lineW / 2; const innerR = r - lineW / 2; const p = new Path(); // 外側を startDeg → endDeg for (let i = 0; i <= segments; i++) { const deg = startDeg + step * i; const rad = deg * Math.PI / 180; const x = cx + outerR * Math.cos(rad); const y = cy + outerR * Math.sin(rad); if (i === 0) { p.move(new Point(x, y)); } else { p.addLine(new Point(x, y)); } } // 内側を endDeg → startDeg(逆順) for (let i = segments; i >= 0; i--) { const deg = startDeg + step * i; const rad = deg * Math.PI / 180; const x = cx + innerR * Math.cos(rad); const y = cy + innerR * Math.sin(rad); p.addLine(new Point(x, y)); } p.closeSubpath(); ctx.addPath(p); ctx.setFillColor(color); ctx.fillPath(); } /** * パーセント中央 + 下部ラベルのリング。 * ラベルも画像内に焼き込むので確実に中央揃えになる。 */ function drawRingSimple(size, pct, label, lineW, fgColor, bgColor, textColor) { const labelH = label ? Math.floor(size * 0.28) : 0; const totalH = size + labelH; const ctx = new DrawContext(); ctx.size = new Size(size, totalH); ctx.opaque = false; ctx.respectScreenScale = true; const cx = size / 2; const cy = size / 2; const r = (size - lineW) / 2 - 1; const segments = 72; drawArcPath(ctx, cx, cy, r, 0, 360, lineW, bgColor, segments); if (pct > 0) { const angle = Math.min(pct, 100) / 100 * 360; drawArcPath(ctx, cx, cy, r, -90, -90 + angle, lineW, fgColor, segments); } const pctStr = `${pct}%`; const pctFontSize = Math.floor(size * 0.32); ctx.setFont(Font.boldSystemFont(pctFontSize)); ctx.setTextColor(textColor); ctx.setTextAlignedCenter(); const pctRectH = pctFontSize * 1.3; const pctY = cy - pctRectH / 2; ctx.drawTextInRect(pctStr, new Rect(0, pctY, size, pctRectH)); // ラベル(リング下部に焼き込み) if (label) { const lblFontSize = Math.max(8, Math.floor(size * 0.22)); ctx.setFont(Font.mediumSystemFont(lblFontSize)); ctx.setTextColor(textColor); ctx.drawTextInRect(label, new Rect(0, size, size, labelH)); } return ctx.getImage(); } // ==================== ロック画面ウィジェット ==================== /** * accessoryCircular: 単一の円グラフ * Parameter で表示項目を選択する */ function buildCircularWidget(items) { const param = args.widgetParameter; const item = getItemByParam(items, param); const w = new ListWidget(); w.backgroundColor = C.lockBg; w.setPadding(0, 0, 0, 0); w.refreshAfterDate = new Date(Date.now() + REFRESH_MINUTES * 60000); if (!item) { const t = w.addText("--"); t.font = Font.boldSystemFont(16); t.textColor = C.lockFg; t.centerAlignText(); w.url = URLScheme.forRunningScript(); return w; } const img = drawRing( 76, item.pct, 5, C.lockFg, C.lockDim, C.lockFg, C.lockFg ); const imgEl = w.addImage(img); imgEl.centerAlignImage(); w.url = URLScheme.forRunningScript(); return w; } /** * accessoryRectangular: 複数の小さい円グラフを横並び * 各リングの下にラベルを表示する */ function buildRectangularWidget(items) { const w = new ListWidget(); w.backgroundColor = C.lockBg; w.setPadding(0, 0, 0, 0); w.refreshAfterDate = new Date(Date.now() + REFRESH_MINUTES * 60000); const display = items.slice(0, 3); if (display.length === 0) { const t = w.addText("データなし"); t.font = Font.regularSystemFont(10); t.textColor = C.lockFg; w.url = URLScheme.forRunningScript(); return w; } const row = w.addStack(); row.layoutHorizontally(); row.centerAlignContent(); for (let i = 0; i < display.length; i++) { if (i > 0) row.addSpacer(4); const item = display[i]; const ringSize = 40; const img = drawRingSimple( ringSize, item.pct, item.short, 3, C.lockFg, C.lockDim, C.lockFg ); const imgEl = row.addImage(img); imgEl.centerAlignImage(); } w.url = URLScheme.forRunningScript(); return w; } /** * accessoryInline: テキストのみ */ function buildInlineWidget(items) { const w = new ListWidget(); const item = getItemByParam(items, args.widgetParameter); if (item) { const t = w.addText(`Claude: ${item.short} ${item.pct}%`); t.font = Font.regularSystemFont(12); } else { w.addText("Claude: --"); } w.url = URLScheme.forRunningScript(); return w; } // ==================== ホーム画面描画 ==================== function drawBar(width, height, pct) { const ctx = new DrawContext(); ctx.size = new Size(width, height); ctx.opaque = false; ctx.respectScreenScale = true; const bgPath = new Path(); bgPath.addRoundedRect(new Rect(0, 0, width, height), 3, 3); ctx.addPath(bgPath); ctx.setFillColor(C.barBg); ctx.fillPath(); if (pct > 0) { const fillW = Math.max(6, Math.round((pct / 100) * width)); const fgPath = new Path(); fgPath.addRoundedRect(new Rect(0, 0, fillW, height), 3, 3); ctx.addPath(fgPath); ctx.setFillColor(barColor(pct)); ctx.fillPath(); } return ctx.getImage(); } function addRow(container, item, barWidth, compact) { const row = container.addStack(); row.layoutVertically(); row.spacing = compact ? 1 : 2; // ラベル行: ラベル(左) + リセット時間(右) const top = row.addStack(); top.layoutHorizontally(); top.centerAlignContent(); const lbl = top.addText(item.label); lbl.font = Font.semiboldSystemFont(compact ? 10 : 11); lbl.textColor = C.text; lbl.lineLimit = 1; top.addSpacer(); if (!compact) { const resetStr = fmtReset(item.reset); if (resetStr) { const rt = top.addText(resetStr); rt.font = Font.regularSystemFont(9); rt.textColor = C.sub; } } // バー行: バー(左) + パーセント(右) const barRow = row.addStack(); barRow.layoutHorizontally(); barRow.centerAlignContent(); const barH = compact ? 5 : 6; const barImg = barRow.addImage(drawBar(barWidth, barH, item.pct)); barImg.imageSize = new Size(barWidth, barH); barRow.addSpacer(); const pctText = barRow.addText(`${item.pct}% 使用済み`); pctText.font = Font.regularSystemFont(9); pctText.textColor = barColor(item.pct); pctText.lineLimit = 1; } function buildWidget(items, timestamp) { const w = new ListWidget(); w.backgroundColor = C.bg; const compact = isSmall(); w.setPadding(compact ? 8 : 16, compact ? 10 : 18, compact ? 8 : 16, compact ? 10 : 18); w.refreshAfterDate = new Date(Date.now() + REFRESH_MINUTES * 60000); // ヘッダー const hdr = w.addStack(); hdr.layoutHorizontally(); hdr.centerAlignContent(); const logoSize = compact ? 12 : 13; const logoImg = hdr.addImage(drawClaudeLogo(logoSize)); logoImg.imageSize = new Size(logoSize, logoSize); hdr.addSpacer(4); const title = hdr.addText("Claude Usage"); title.font = Font.boldSystemFont(compact ? 11 : 12); title.textColor = C.text; hdr.addSpacer(); const ageLabel = fmtAge(timestamp); if (ageLabel) { const ts = hdr.addText(ageLabel); ts.font = Font.regularSystemFont(9); ts.textColor = cacheAgeColor(timestamp); } w.addSpacer(compact ? 4 : 6); // データ行(セクション見出し付き) const display = compact ? items.slice(0, 2) : items; const barWidth = getBarWidth(); let lastSection = null; for (let i = 0; i < display.length; i++) { const item = display[i]; if (!compact && item.section && item.section !== lastSection) { if (lastSection !== null) w.addSpacer(4); const sec = w.addText(item.section); sec.font = Font.boldSystemFont(9); sec.textColor = C.sub; w.addSpacer(2); lastSection = item.section; } else if (i > 0) { w.addSpacer(3); } addRow(w, item, barWidth, compact); } if (display.length === 0) { const noData = w.addText("データなし"); noData.font = Font.regularSystemFont(11); noData.textColor = C.sub; } w.addSpacer(); w.url = URLScheme.forRunningScript(); return w; } function buildErrorWidget(errType) { const w = new ListWidget(); w.backgroundColor = C.bg; w.setPadding(14, 14, 14, 14); w.refreshAfterDate = new Date(Date.now() + ERROR_REFRESH_MINUTES * 60000); const hdr = w.addStack(); hdr.layoutHorizontally(); hdr.centerAlignContent(); const ic = hdr.addText("⚠"); ic.font = Font.boldSystemFont(14); hdr.addSpacer(6); const t = hdr.addText("Claude Usage"); t.font = Font.boldSystemFont(13); t.textColor = C.text; w.addSpacer(8); const tt = w.addText(errTitle(errType)); tt.font = Font.semiboldSystemFont(12); tt.textColor = C.red; w.addSpacer(4); const m = w.addText(errMessage(errType)); m.font = Font.regularSystemFont(10); m.textColor = C.sub; m.minimumScaleFactor = 0.7; w.addSpacer(); w.url = URLScheme.forRunningScript(); return w; } function buildSetupWidget() { const w = new ListWidget(); w.backgroundColor = C.bg; w.setPadding(14, 14, 14, 14); const hdr = w.addStack(); hdr.layoutHorizontally(); hdr.centerAlignContent(); const logoImg = hdr.addImage(drawClaudeLogo(14)); logoImg.imageSize = new Size(14, 14); hdr.addSpacer(4); const t = hdr.addText("Claude Usage"); t.font = Font.boldSystemFont(14); t.textColor = C.accent; w.addSpacer(8); const m = w.addText("タップして初期設定"); m.font = Font.regularSystemFont(11); m.textColor = C.text; w.addSpacer(); w.url = URLScheme.forRunningScript(); return w; } // ==================== エラー表示(アプリ内実行時) ==================== async function showErrorAlert(err) { const a = new Alert(); a.title = "Claude Usage"; a.message = "エラー: " + errMessage(err); a.addAction("OK"); a.addAction("キャッシュ & orgId クリア"); const idx = await a.presentAlert(); if (idx === 1) { if (Keychain.contains(KEYCHAIN_ORG)) Keychain.remove(KEYCHAIN_ORG); if (fm.fileExists(cachePath)) fm.remove(cachePath); const done = new Alert(); done.title = "クリア完了"; done.message = "次回実行時に再取得します"; done.addAction("OK"); await done.presentAlert(); } } // ==================== メイン ==================== async function main() { // --- ウィジェットモード --- if (config.runsInWidget) { const cached = loadFromCache(); if (!cached.ok) { // ロック画面のセットアップは小さいので簡易表示 if (isLockScreen()) { const w = new ListWidget(); w.addText("--"); w.url = URLScheme.forRunningScript(); Script.setWidget(w); } else { Script.setWidget(buildSetupWidget()); } return Script.complete(); } const items = parse(cached.data); const family = config.widgetFamily; if (family === "accessoryCircular") { Script.setWidget(buildCircularWidget(items)); } else if (family === "accessoryRectangular") { Script.setWidget(buildRectangularWidget(items)); } else if (family === "accessoryInline") { Script.setWidget(buildInlineWidget(items)); } else { Script.setWidget(buildWidget(items, cached.timestamp)); } return Script.complete(); } // --- アプリ内実行: WebView でライブ取得 --- const result = await fetchUsageLive(); if (!result.ok) { const cached = loadFromCache(); if (cached.ok) { const items = parse(cached.data); const w = buildWidget(items, cached.timestamp); const warn = new Alert(); warn.title = "ライブ取得失敗"; warn.message = "エラー: " + errMessage(result.err) + "\n\n" + "キャッシュデータを表示します(" + (fmtAge(cached.timestamp) || "") + ")"; warn.addAction("OK"); await warn.presentAlert(); await w.presentMedium(); return Script.complete(); } const w = buildErrorWidget(result.err); await showErrorAlert(result.err); await w.presentMedium(); return Script.complete(); } // 成功 const cache = readCache(); const items = parse(result.data); const w = buildWidget(items, cache ? cache.timestamp : null); await w.presentMedium(); Script.complete(); } await main(); |
↑ 長いので折り畳んでいます。
見出しをクリックすると展開します。
ウィジットに登録
ホーム画面を長押し > 編集 > ウィジットを追加 > Scriptable で、中サイズ(横長)のウィジットを追加します。
Scriptable の設定画面が出るので、下記のように指定します。
- Script: Claude Usage(スクリプトに付けた名前)
- When Interacting: Run Script
- Parameter: (なし)

ウィジットに登録するとこんな感じです。

待受画面に登録
待受画面にウィジットを追加する場合は大きい方のサイズ(横長)を選択すれば、3 つのグラフが並ぶようになっています。
Claude の公式アプリと並べるといい感じですね!

小さい方のサイズを選択した場合には、Scriptable の設定画面でパラメーターを指定すると、個別のグラフを表示できます。
- session
- weekly
- sonnet
- opus(動作せず)
参考にしたサイト
ClaudeCodeの使用率をVSCode上でチェックできるようにしてみた - Qiita
Claude Code、VSCodeでも便利ですよね! でも使用率がWebの設定画面からしか把握できないのはイケてないですよね! 作りました ないなら Claude Code で作ってしまえばよい。 ということで、VSCodeの拡張機能を作...
GitHub - hamed-elfayome/Claude-Usage-Tracker: Native macOS menu bar app for tracking Claude AI usage limits in real-time. Built with Swift/SwiftUI.
Native macOS menu bar app for tracking Claude AI usage limits in real-time. Built with Swift/SwiftUI. - hamed-elfayome/C...

コメント