README 改为与 QuotaBar 统一的版式

- 顶部居中 Logo、同配色徽章(版本、下载、星标、最近提交、提交频率、CI、macOS、许可证)
- 中英文两份 README,最近更新由 CHANGELOG.md 生成,附提交活跃度热力图与星标历史
- 新增 CHANGELOG.md、ARCHITECTURE.md、MIT LICENSE、Scripts/sync_changelog.py、CI 工作流
- README 图片移到 Assets/readme
This commit is contained in:
gentpan committed 2026-09-13 03:12:30 +05:00
1 parent 7ea40d98e6
commit 3daa7a4541
17 files changed
+1326 -126

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build-and-test:
runs-on: macos-15
steps:
- uses: actions/checkout@v7
# CommandLineTools alone cannot expand SwiftUI's @Observable / @Entry
# macros — the plugins ship only with full Xcode. Use the runner's
# default Xcode rather than pinning a version that may not be installed
# on a future image.
- name: Select Xcode
run: |
ls -d /Applications/Xcode*.app
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
swift --version
- name: Install XcodeGen
run: brew install xcodegen
- name: Test
run: make test
- name: Build app
run: make build CONFIG=Release
- name: Verify bundle
run: |
APP=build/DerivedData/Build/Products/Release/OpenStats.app
test -x "$APP/Contents/MacOS/OpenStats"
test -x "$APP/Contents/MacOS/OpenStatsHelper"
test -f "$APP/Contents/Library/LaunchDaemons/com.openstats.helper.plist"
test -f "$APP/Contents/Resources/AppIcon.icns"
codesign --verify --verbose "$APP"
# Smoke test: the panel is a menu-bar popover with no window to drive in
# CI, so render it off-screen instead. A view that traps on layout fails
# here rather than the first time a user clicks the menu-bar item.
- name: Render UI snapshots
run: |
build/DerivedData/Build/Products/Release/OpenStats.app/Contents/MacOS/OpenStats --snapshot "$PWD/snapshots"
test "$(ls snapshots/*.png | wc -l)" -ge 20
- uses: actions/upload-artifact@v7
with:
name: ui-snapshots
path: snapshots/
+105
View File
@@ -0,0 +1,105 @@
# OpenStats
macOS 14+ menu-bar system monitor. Swift 6 language mode, AppKit status item and panels
hosting SwiftUI, no third-party dependencies. The Xcode project is generated from
`project.yml` by XcodeGen; everything testable lives in the local Swift package.
- `App/` — `main.swift` and the asset catalog.
- `Helper/` — the privileged helper (`OpenStatsHelper`) and its launchd plist, embedded in
the app bundle for `SMAppService.daemon`.
- `Packages/OpenStatsKit/Sources/SMC` — the AppleSMC user client, fan control, temperature
key discovery.
- `Packages/OpenStatsKit/Sources/Metrics` — one sampler per metric and `MetricsHub`.
- `Packages/OpenStatsKit/Sources/Cleaner` — cleanup rules, `SafetyGuard`, `CleanEngine`.
- `Packages/OpenStatsKit/Sources/HelperShared` — the XPC protocol and maintenance commands
shared by the app and the helper.
- `Packages/OpenStatsKit/Sources/OpenStatsUI` — design tokens, panel pages, settings,
menu-bar renderer, app controller, snapshot renderer.
- `Packages/OpenStatsKit/Tests` — metrics, SMC decoding and cleanup safety.
## Build
**A full Xcode is required.** CommandLineTools does not ship the SwiftUI macro plugins.
```bash
brew install xcodegen
make build # Debug
make install # Release into /Applications
make test # swift test in the package
```
Builds are ad-hoc signed (`CODE_SIGN_IDENTITY: "-"` in `project.yml`). For a release,
switch to a Developer ID identity, keep hardened runtime on, notarize, and set
`HelperConstants.teamIdentifier` so the helper's code-signing requirement includes the team.
## Sampling
`MetricsHub` is an actor that runs one loop. `AppModel.demand` describes what is on screen —
which menu-bar items are enabled, whether the panel is open and on which tab — and the hub
samples only that: CPU always; memory and network cheaply; GPU, disk, processes, sensors and
fans only when a visible surface needs them. Disk is read at most every 30 s, battery every
10 s. The loop pauses on screen sleep, system sleep and session switch.
Things that are easy to get wrong and are handled on purpose:
- `host_processor_info` returns kernel-allocated memory; it is `vm_deallocate`d every sample.
- `getifaddrs`' `if_data` counters are 32-bit and wrap at 4 GiB; network uses
`NET_RT_IFLIST2` and `if_data64`.
- Memory follows Activity Monitor: app memory is `internal − purgeable` pages; the page size
comes from `host_page_size` (16 KB on Apple Silicon).
- Process CPU time and wall time are both in mach absolute units, so their ratio needs no
timebase conversion.
- Core types come from `hw.perflevelN`; logical CPUs are numbered from the lowest
performance level up.
## SMC
Temperatures are not hard-coded per chip. On first use the sampler enumerates every SMC key
once (≈3,800 keys in about 10 ms on an M5 Max), groups `Tp*`/`Te*` as CPU, `Tg*` as GPU,
`Tm*` as memory, `TB*` as battery and `Ts0P`/`Ts1P` as palm rest, keeps keys whose first
reading is plausible, and samples at most 12 per group.
Fans are read without privileges. Writing needs root and goes through the helper. On M5
the mode key (`F0md`) accepts a direct write; M1–M4 first need `Ftst=1` and a pause while
`thermalmonitord` lets go. The firmware records only manual or automatic, not who set it, so
a fan in manual mode that OpenStats did not set is shown as controlled by another program.
## Helper
Registered with `SMAppService.daemon`; `RunAtLoad` so that an unclean exit is repaired at
boot. The XPC interface is a fixed list of operations — no arbitrary commands — and each
connection gets `setCodeSigningRequirement`. State that must be undone (manual fans, disabled
sleep) is persisted to `/Library/Application Support/OpenStats/helper-state.plist` and
reverted when the last client disconnects or at the next start. The helper exits after 30 s
without clients.
## Cleanup
Every rule lists candidate items; every item passes `SafetyGuard` twice — at scan and again
right before deletion, because apps start in between.
- Deny by default: an item must sit strictly inside an allow-listed root under the home
folder, never be the root itself, contain no `..` or control characters, and still pass
after symlinks are resolved (resolution can only reject, never allow).
- Under `Application Support`, only folders named like caches (`Code Cache`, `GPUCache`,
`CacheStorage`, …) are allowed.
- Any path component matching a protected keyword (keychains, password managers, VPNs,
cookies, history, …) is rejected.
- Reverse-DNS cache folders whose app is running are skipped; browser rules are blocked while
the browser runs; items modified in the last two minutes count as in use.
Regenerable caches are deleted outright; user files go to the Trash. Each action is appended
to `~/Library/Logs/OpenStats/cleanup.log` as a JSON line.
## Panel
A borderless, non-activating `NSPanel` under the status item, hosting SwiftUI inside
`NSGlassEffectView` on macOS 26+ (`NSVisualEffectView` before). The SwiftUI tree is created on
open and destroyed on close, so a hidden panel costs nothing. Height comes from measuring a
flat, scroll-free copy of the same view; placeholders keep that height stable until data
arrives, and the window is resized without animation because animating it makes SwiftUI
re-lay out every frame. Charts are drawn with `Canvas`, not Swift Charts.
`--snapshot <dir>` renders every panel tab, settings section and the menu bar in light and
dark, through real `NSHostingView`s in off-screen windows — `ImageRenderer` washes out pages
that contain bitmaps.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

+199
View File
@@ -0,0 +1,199 @@
<svg xmlns="http://www.w3.org/2000/svg" width="394" height="156" viewBox="0 0 394 156" role="img">
<title>4 commits in the last 26 weeks</title>
<g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,PingFang SC,sans-serif" font-size="9" fill="#8b949e">
<text x="58" y="12">Apr</text>
<text x="114" y="12">May</text>
<text x="184" y="12">Jun</text>
<text x="240" y="12">Jul</text>
<text x="296" y="12">Aug</text>
<text x="366" y="12">Sep</text>
<text x="0" y="41">Mon</text>
<text x="0" y="69">Wed</text>
<text x="0" y="97">Fri</text>
</g>
<rect x="30" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-22</title></rect>
<rect x="30" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-23</title></rect>
<rect x="30" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-24</title></rect>
<rect x="30" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-25</title></rect>
<rect x="30" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-26</title></rect>
<rect x="30" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-27</title></rect>
<rect x="30" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-28</title></rect>
<rect x="44" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-29</title></rect>
<rect x="44" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-30</title></rect>
<rect x="44" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-03-31</title></rect>
<rect x="44" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-01</title></rect>
<rect x="44" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-02</title></rect>
<rect x="44" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-03</title></rect>
<rect x="44" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-04</title></rect>
<rect x="58" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-05</title></rect>
<rect x="58" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-06</title></rect>
<rect x="58" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-07</title></rect>
<rect x="58" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-08</title></rect>
<rect x="58" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-09</title></rect>
<rect x="58" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-10</title></rect>
<rect x="58" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-11</title></rect>
<rect x="72" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-12</title></rect>
<rect x="72" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-13</title></rect>
<rect x="72" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-14</title></rect>
<rect x="72" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-15</title></rect>
<rect x="72" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-16</title></rect>
<rect x="72" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-17</title></rect>
<rect x="72" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-18</title></rect>
<rect x="86" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-19</title></rect>
<rect x="86" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-20</title></rect>
<rect x="86" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-21</title></rect>
<rect x="86" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-22</title></rect>
<rect x="86" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-23</title></rect>
<rect x="86" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-24</title></rect>
<rect x="86" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-25</title></rect>
<rect x="100" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-26</title></rect>
<rect x="100" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-27</title></rect>
<rect x="100" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-28</title></rect>
<rect x="100" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-29</title></rect>
<rect x="100" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-04-30</title></rect>
<rect x="100" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-01</title></rect>
<rect x="100" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-02</title></rect>
<rect x="114" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-03</title></rect>
<rect x="114" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-04</title></rect>
<rect x="114" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-05</title></rect>
<rect x="114" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-06</title></rect>
<rect x="114" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-07</title></rect>
<rect x="114" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-08</title></rect>
<rect x="114" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-09</title></rect>
<rect x="128" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-10</title></rect>
<rect x="128" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-11</title></rect>
<rect x="128" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-12</title></rect>
<rect x="128" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-13</title></rect>
<rect x="128" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-14</title></rect>
<rect x="128" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-15</title></rect>
<rect x="128" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-16</title></rect>
<rect x="142" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-17</title></rect>
<rect x="142" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-18</title></rect>
<rect x="142" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-19</title></rect>
<rect x="142" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-20</title></rect>
<rect x="142" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-21</title></rect>
<rect x="142" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-22</title></rect>
<rect x="142" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-23</title></rect>
<rect x="156" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-24</title></rect>
<rect x="156" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-25</title></rect>
<rect x="156" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-26</title></rect>
<rect x="156" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-27</title></rect>
<rect x="156" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-28</title></rect>
<rect x="156" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-29</title></rect>
<rect x="156" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-30</title></rect>
<rect x="170" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-05-31</title></rect>
<rect x="170" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-01</title></rect>
<rect x="170" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-02</title></rect>
<rect x="170" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-03</title></rect>
<rect x="170" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-04</title></rect>
<rect x="170" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-05</title></rect>
<rect x="170" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-06</title></rect>
<rect x="184" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-07</title></rect>
<rect x="184" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-08</title></rect>
<rect x="184" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-09</title></rect>
<rect x="184" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-10</title></rect>
<rect x="184" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-11</title></rect>
<rect x="184" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-12</title></rect>
<rect x="184" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-13</title></rect>
<rect x="198" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-14</title></rect>
<rect x="198" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-15</title></rect>
<rect x="198" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-16</title></rect>
<rect x="198" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-17</title></rect>
<rect x="198" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-18</title></rect>
<rect x="198" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-19</title></rect>
<rect x="198" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-20</title></rect>
<rect x="212" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-21</title></rect>
<rect x="212" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-22</title></rect>
<rect x="212" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-23</title></rect>
<rect x="212" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-24</title></rect>
<rect x="212" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-25</title></rect>
<rect x="212" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-26</title></rect>
<rect x="212" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-27</title></rect>
<rect x="226" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-28</title></rect>
<rect x="226" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-29</title></rect>
<rect x="226" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-06-30</title></rect>
<rect x="226" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-01</title></rect>
<rect x="226" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-02</title></rect>
<rect x="226" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-03</title></rect>
<rect x="226" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-04</title></rect>
<rect x="240" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-05</title></rect>
<rect x="240" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-06</title></rect>
<rect x="240" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-07</title></rect>
<rect x="240" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-08</title></rect>
<rect x="240" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-09</title></rect>
<rect x="240" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-10</title></rect>
<rect x="240" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-11</title></rect>
<rect x="254" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-12</title></rect>
<rect x="254" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-13</title></rect>
<rect x="254" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-14</title></rect>
<rect x="254" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-15</title></rect>
<rect x="254" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-16</title></rect>
<rect x="254" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-17</title></rect>
<rect x="254" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-18</title></rect>
<rect x="268" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-19</title></rect>
<rect x="268" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-20</title></rect>
<rect x="268" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-21</title></rect>
<rect x="268" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-22</title></rect>
<rect x="268" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-23</title></rect>
<rect x="268" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-24</title></rect>
<rect x="268" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-25</title></rect>
<rect x="282" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-26</title></rect>
<rect x="282" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-27</title></rect>
<rect x="282" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-28</title></rect>
<rect x="282" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-29</title></rect>
<rect x="282" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-30</title></rect>
<rect x="282" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-07-31</title></rect>
<rect x="282" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-01</title></rect>
<rect x="296" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-02</title></rect>
<rect x="296" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-03</title></rect>
<rect x="296" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-04</title></rect>
<rect x="296" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-05</title></rect>
<rect x="296" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-06</title></rect>
<rect x="296" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-07</title></rect>
<rect x="296" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-08</title></rect>
<rect x="310" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-09</title></rect>
<rect x="310" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-10</title></rect>
<rect x="310" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-11</title></rect>
<rect x="310" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-12</title></rect>
<rect x="310" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-13</title></rect>
<rect x="310" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-14</title></rect>
<rect x="310" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-15</title></rect>
<rect x="324" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-16</title></rect>
<rect x="324" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-17</title></rect>
<rect x="324" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-18</title></rect>
<rect x="324" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-19</title></rect>
<rect x="324" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-20</title></rect>
<rect x="324" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-21</title></rect>
<rect x="324" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-22</title></rect>
<rect x="338" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-23</title></rect>
<rect x="338" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-24</title></rect>
<rect x="338" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-25</title></rect>
<rect x="338" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-26</title></rect>
<rect x="338" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-27</title></rect>
<rect x="338" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-28</title></rect>
<rect x="338" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-29</title></rect>
<rect x="352" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-30</title></rect>
<rect x="352" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-08-31</title></rect>
<rect x="352" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-01</title></rect>
<rect x="352" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-02</title></rect>
<rect x="352" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-03</title></rect>
<rect x="352" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-04</title></rect>
<rect x="352" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-05</title></rect>
<rect x="366" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-06</title></rect>
<rect x="366" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-07</title></rect>
<rect x="366" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-08</title></rect>
<rect x="366" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-09</title></rect>
<rect x="366" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-10</title></rect>
<rect x="366" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-11</title></rect>
<rect x="366" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>0 commits on 2026-09-12</title></rect>
<rect x="380" y="18" width="11" height="11" rx="2" fill="#216e39"><title>4 commits on 2026-09-13</title></rect>
<g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,PingFang SC,sans-serif" font-size="10" fill="#8b949e">
<text x="30" y="146">4 commits in the last 26 weeks · through 2026-09-13</text>
<text x="286" y="130" text-anchor="end">Less</text>
<rect x="290" y="120" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"/>
<rect x="304" y="120" width="11" height="11" rx="2" fill="#9be9a8"/>
<rect x="318" y="120" width="11" height="11" rx="2" fill="#40c463"/>
<rect x="332" y="120" width="11" height="11" rx="2" fill="#30a14e"/>
<rect x="346" y="120" width="11" height="11" rx="2" fill="#216e39"/>
<text x="362" y="130">More</text>
</g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

+199
View File
@@ -0,0 +1,199 @@
<svg xmlns="http://www.w3.org/2000/svg" width="394" height="156" viewBox="0 0 394 156" role="img">
<title>近 26 周共 4 次提交</title>
<g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,PingFang SC,sans-serif" font-size="9" fill="#8b949e">
<text x="58" y="12">4月</text>
<text x="114" y="12">5月</text>
<text x="184" y="12">6月</text>
<text x="240" y="12">7月</text>
<text x="296" y="12">8月</text>
<text x="366" y="12">9月</text>
<text x="0" y="41">一</text>
<text x="0" y="69">三</text>
<text x="0" y="97">五</text>
</g>
<rect x="30" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-22:0 次提交</title></rect>
<rect x="30" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-23:0 次提交</title></rect>
<rect x="30" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-24:0 次提交</title></rect>
<rect x="30" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-25:0 次提交</title></rect>
<rect x="30" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-26:0 次提交</title></rect>
<rect x="30" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-27:0 次提交</title></rect>
<rect x="30" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-28:0 次提交</title></rect>
<rect x="44" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-29:0 次提交</title></rect>
<rect x="44" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-30:0 次提交</title></rect>
<rect x="44" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-03-31:0 次提交</title></rect>
<rect x="44" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-01:0 次提交</title></rect>
<rect x="44" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-02:0 次提交</title></rect>
<rect x="44" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-03:0 次提交</title></rect>
<rect x="44" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-04:0 次提交</title></rect>
<rect x="58" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-05:0 次提交</title></rect>
<rect x="58" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-06:0 次提交</title></rect>
<rect x="58" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-07:0 次提交</title></rect>
<rect x="58" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-08:0 次提交</title></rect>
<rect x="58" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-09:0 次提交</title></rect>
<rect x="58" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-10:0 次提交</title></rect>
<rect x="58" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-11:0 次提交</title></rect>
<rect x="72" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-12:0 次提交</title></rect>
<rect x="72" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-13:0 次提交</title></rect>
<rect x="72" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-14:0 次提交</title></rect>
<rect x="72" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-15:0 次提交</title></rect>
<rect x="72" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-16:0 次提交</title></rect>
<rect x="72" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-17:0 次提交</title></rect>
<rect x="72" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-18:0 次提交</title></rect>
<rect x="86" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-19:0 次提交</title></rect>
<rect x="86" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-20:0 次提交</title></rect>
<rect x="86" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-21:0 次提交</title></rect>
<rect x="86" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-22:0 次提交</title></rect>
<rect x="86" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-23:0 次提交</title></rect>
<rect x="86" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-24:0 次提交</title></rect>
<rect x="86" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-25:0 次提交</title></rect>
<rect x="100" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-26:0 次提交</title></rect>
<rect x="100" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-27:0 次提交</title></rect>
<rect x="100" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-28:0 次提交</title></rect>
<rect x="100" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-29:0 次提交</title></rect>
<rect x="100" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-04-30:0 次提交</title></rect>
<rect x="100" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-01:0 次提交</title></rect>
<rect x="100" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-02:0 次提交</title></rect>
<rect x="114" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-03:0 次提交</title></rect>
<rect x="114" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-04:0 次提交</title></rect>
<rect x="114" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-05:0 次提交</title></rect>
<rect x="114" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-06:0 次提交</title></rect>
<rect x="114" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-07:0 次提交</title></rect>
<rect x="114" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-08:0 次提交</title></rect>
<rect x="114" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-09:0 次提交</title></rect>
<rect x="128" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-10:0 次提交</title></rect>
<rect x="128" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-11:0 次提交</title></rect>
<rect x="128" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-12:0 次提交</title></rect>
<rect x="128" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-13:0 次提交</title></rect>
<rect x="128" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-14:0 次提交</title></rect>
<rect x="128" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-15:0 次提交</title></rect>
<rect x="128" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-16:0 次提交</title></rect>
<rect x="142" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-17:0 次提交</title></rect>
<rect x="142" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-18:0 次提交</title></rect>
<rect x="142" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-19:0 次提交</title></rect>
<rect x="142" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-20:0 次提交</title></rect>
<rect x="142" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-21:0 次提交</title></rect>
<rect x="142" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-22:0 次提交</title></rect>
<rect x="142" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-23:0 次提交</title></rect>
<rect x="156" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-24:0 次提交</title></rect>
<rect x="156" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-25:0 次提交</title></rect>
<rect x="156" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-26:0 次提交</title></rect>
<rect x="156" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-27:0 次提交</title></rect>
<rect x="156" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-28:0 次提交</title></rect>
<rect x="156" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-29:0 次提交</title></rect>
<rect x="156" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-30:0 次提交</title></rect>
<rect x="170" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-05-31:0 次提交</title></rect>
<rect x="170" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-01:0 次提交</title></rect>
<rect x="170" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-02:0 次提交</title></rect>
<rect x="170" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-03:0 次提交</title></rect>
<rect x="170" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-04:0 次提交</title></rect>
<rect x="170" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-05:0 次提交</title></rect>
<rect x="170" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-06:0 次提交</title></rect>
<rect x="184" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-07:0 次提交</title></rect>
<rect x="184" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-08:0 次提交</title></rect>
<rect x="184" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-09:0 次提交</title></rect>
<rect x="184" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-10:0 次提交</title></rect>
<rect x="184" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-11:0 次提交</title></rect>
<rect x="184" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-12:0 次提交</title></rect>
<rect x="184" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-13:0 次提交</title></rect>
<rect x="198" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-14:0 次提交</title></rect>
<rect x="198" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-15:0 次提交</title></rect>
<rect x="198" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-16:0 次提交</title></rect>
<rect x="198" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-17:0 次提交</title></rect>
<rect x="198" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-18:0 次提交</title></rect>
<rect x="198" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-19:0 次提交</title></rect>
<rect x="198" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-20:0 次提交</title></rect>
<rect x="212" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-21:0 次提交</title></rect>
<rect x="212" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-22:0 次提交</title></rect>
<rect x="212" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-23:0 次提交</title></rect>
<rect x="212" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-24:0 次提交</title></rect>
<rect x="212" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-25:0 次提交</title></rect>
<rect x="212" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-26:0 次提交</title></rect>
<rect x="212" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-27:0 次提交</title></rect>
<rect x="226" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-28:0 次提交</title></rect>
<rect x="226" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-29:0 次提交</title></rect>
<rect x="226" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-06-30:0 次提交</title></rect>
<rect x="226" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-01:0 次提交</title></rect>
<rect x="226" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-02:0 次提交</title></rect>
<rect x="226" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-03:0 次提交</title></rect>
<rect x="226" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-04:0 次提交</title></rect>
<rect x="240" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-05:0 次提交</title></rect>
<rect x="240" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-06:0 次提交</title></rect>
<rect x="240" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-07:0 次提交</title></rect>
<rect x="240" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-08:0 次提交</title></rect>
<rect x="240" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-09:0 次提交</title></rect>
<rect x="240" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-10:0 次提交</title></rect>
<rect x="240" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-11:0 次提交</title></rect>
<rect x="254" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-12:0 次提交</title></rect>
<rect x="254" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-13:0 次提交</title></rect>
<rect x="254" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-14:0 次提交</title></rect>
<rect x="254" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-15:0 次提交</title></rect>
<rect x="254" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-16:0 次提交</title></rect>
<rect x="254" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-17:0 次提交</title></rect>
<rect x="254" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-18:0 次提交</title></rect>
<rect x="268" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-19:0 次提交</title></rect>
<rect x="268" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-20:0 次提交</title></rect>
<rect x="268" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-21:0 次提交</title></rect>
<rect x="268" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-22:0 次提交</title></rect>
<rect x="268" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-23:0 次提交</title></rect>
<rect x="268" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-24:0 次提交</title></rect>
<rect x="268" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-25:0 次提交</title></rect>
<rect x="282" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-26:0 次提交</title></rect>
<rect x="282" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-27:0 次提交</title></rect>
<rect x="282" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-28:0 次提交</title></rect>
<rect x="282" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-29:0 次提交</title></rect>
<rect x="282" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-30:0 次提交</title></rect>
<rect x="282" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-07-31:0 次提交</title></rect>
<rect x="282" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-01:0 次提交</title></rect>
<rect x="296" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-02:0 次提交</title></rect>
<rect x="296" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-03:0 次提交</title></rect>
<rect x="296" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-04:0 次提交</title></rect>
<rect x="296" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-05:0 次提交</title></rect>
<rect x="296" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-06:0 次提交</title></rect>
<rect x="296" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-07:0 次提交</title></rect>
<rect x="296" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-08:0 次提交</title></rect>
<rect x="310" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-09:0 次提交</title></rect>
<rect x="310" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-10:0 次提交</title></rect>
<rect x="310" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-11:0 次提交</title></rect>
<rect x="310" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-12:0 次提交</title></rect>
<rect x="310" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-13:0 次提交</title></rect>
<rect x="310" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-14:0 次提交</title></rect>
<rect x="310" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-15:0 次提交</title></rect>
<rect x="324" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-16:0 次提交</title></rect>
<rect x="324" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-17:0 次提交</title></rect>
<rect x="324" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-18:0 次提交</title></rect>
<rect x="324" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-19:0 次提交</title></rect>
<rect x="324" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-20:0 次提交</title></rect>
<rect x="324" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-21:0 次提交</title></rect>
<rect x="324" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-22:0 次提交</title></rect>
<rect x="338" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-23:0 次提交</title></rect>
<rect x="338" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-24:0 次提交</title></rect>
<rect x="338" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-25:0 次提交</title></rect>
<rect x="338" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-26:0 次提交</title></rect>
<rect x="338" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-27:0 次提交</title></rect>
<rect x="338" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-28:0 次提交</title></rect>
<rect x="338" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-29:0 次提交</title></rect>
<rect x="352" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-30:0 次提交</title></rect>
<rect x="352" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-08-31:0 次提交</title></rect>
<rect x="352" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-01:0 次提交</title></rect>
<rect x="352" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-02:0 次提交</title></rect>
<rect x="352" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-03:0 次提交</title></rect>
<rect x="352" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-04:0 次提交</title></rect>
<rect x="352" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-05:0 次提交</title></rect>
<rect x="366" y="18" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-06:0 次提交</title></rect>
<rect x="366" y="32" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-07:0 次提交</title></rect>
<rect x="366" y="46" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-08:0 次提交</title></rect>
<rect x="366" y="60" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-09:0 次提交</title></rect>
<rect x="366" y="74" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-10:0 次提交</title></rect>
<rect x="366" y="88" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-11:0 次提交</title></rect>
<rect x="366" y="102" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"><title>2026-09-12:0 次提交</title></rect>
<rect x="380" y="18" width="11" height="11" rx="2" fill="#216e39"><title>2026-09-13:4 次提交</title></rect>
<g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,PingFang SC,sans-serif" font-size="10" fill="#8b949e">
<text x="30" y="146">近 26 周共 4 次提交 · 截至 2026-09-13</text>
<text x="286" y="130" text-anchor="end">少</text>
<rect x="290" y="120" width="11" height="11" rx="2" fill="rgba(139,148,158,0.18)"/>
<rect x="304" y="120" width="11" height="11" rx="2" fill="#9be9a8"/>
<rect x="318" y="120" width="11" height="11" rx="2" fill="#40c463"/>
<rect x="332" y="120" width="11" height="11" rx="2" fill="#30a14e"/>
<rect x="346" y="120" width="11" height="11" rx="2" fill="#216e39"/>
<text x="362" y="130">多</text>
</g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 337 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 326 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

+40
View File
@@ -0,0 +1,40 @@
# 更新日志
记录 OpenStats 应用的功能新增、样式调整和问题修复,按版本与日期倒序排列。
官网、构建与发布脚本等不影响应用本身的改动不在此记录。
## 未发布
### 2026-09-13
#### 新增
- 菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。CPU、GPU、内存可以分别选择数字、柱状图、圆环、饼图、柱状图加数字五种样式;网速两行显示,绿点上传、蓝点下载,带完整单位。
- 下拉面板分为概览、进程、散热、防休眠、清理五页。概览页顶部是健康评分和芯片、内存、系统版本、运行时长、机型徽章,下面是 CPU 柱状历史、GPU 折线、内存面积图、磁盘、网络双线、风扇三列卡片,以及核心负载、电池环形图、高占用进程和快捷开关。
- CPU 按超级核、性能核、能效核分组显示各核心负载;内存口径与活动监视器一致,并显示内存压力;网速读取 64 位计数器,大流量下不会回绕。
- 温度与风扇:启动时枚举一次 SMC 键并按前缀归类,不按芯片型号硬编码,显示 CPU、GPU、内存、电池和掌托温度。
- 按界面需要分级采样:面板收起时只采集菜单栏用到的指标,锁屏、屏幕休眠和系统睡眠时暂停。
- 风扇调速:自动、降温、强冷、自定义四种模式。自定义模式下 CPU 达到安全温度会自动交还系统控制,应用退出或断开连接时风扇恢复自动。
- 防休眠:屏幕保持常亮、仅系统不休眠、合盖后继续运行,可设定持续时间;使用电池且电量低于下限时自动关闭合盖运行。
- 辅助工具:通过 SMAppService 注册,只提供设置风扇转速、切换合盖不睡眠、刷新 DNS、释放内存几个固定操作,并校验调用方签名;异常退出后下次开机自动恢复风扇与睡眠设置。
- 清理:扫描应用缓存、日志与崩溃报告、Chrome / Edge / Brave / Arc / Firefox / Safari 缓存、Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。按类别显示大小,可展开查看具体项目;清理前二次确认,操作写入 `~/Library/Logs/OpenStats/cleanup.log`。
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏显示项与样式、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 外观可选跟随系统、浅色、深色;macOS 26 起面板使用液态玻璃。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染面板、设置和菜单栏的浅色与深色截图。
#### 样式
- 菜单栏字号对齐常见菜单栏监控工具:标签 7pt 细体、数值 12pt 常规、网速 9pt 细体,指标之间留出更宽的间距。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
- 更换应用图标,面板左上角和关于页使用同一图标。
- 首次启动默认在菜单栏显示 CPU、内存与网速。
#### 修复
- 深色菜单栏上 CPU、RAM 小标签颜色过深,几乎看不清。
- 面板打开后高度会再调整一次、内容轻微回弹,看起来在抖动;面板打开时点击菜单栏图标关闭,会立刻又被打开。
- 风扇被其他程序设为手动模式时,误显示为由 OpenStats 控制。
- 风扇转速、PID 等数字出现千分位分隔符,例如 1,350 RPM。
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 GiantAccel, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+216 -126
View File
@@ -1,170 +1,260 @@
<p align="center"> <div align="center">
<img src="App/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" width="112" alt="OpenStats 图标">
</p>
<h1 align="center">OpenStats</h1> <img src="Assets/icon.png" alt="OpenStats" width="112" height="112">
<p align="center"> # OpenStats
<b>轻量、原生的 macOS 菜单栏系统监控</b><br>
CPU · GPU · 内存 · 网络 · 磁盘 · 电池 · 温度 · 风扇调速 · 防休眠<br>
<a href="https://getopenstats.com">getopenstats.com</a>
</p>
<p align="center"> **Your Mac at a glance — CPU, GPU, memory, network and temperatures in the menu bar, with fan control, keep-awake and one-click cleanup.**
<img src="https://img.shields.io/badge/macOS-14%2B-111827?logo=apple&logoColor=white" alt="macOS 14+">
<img src="https://img.shields.io/badge/Swift-6-F05138?logo=swift&logoColor=white" alt="Swift 6">
<img src="https://img.shields.io/badge/Apple%20Silicon-原生-2563EB" alt="Apple Silicon">
<img src="https://img.shields.io/badge/状态-开发中-D97706" alt="开发中">
</p>
<p align="center"> [![Release](https://img.shields.io/github/v/release/gentpan/OpenStats?color=6ee02b&label=release)](https://github.com/gentpan/OpenStats/releases/latest)
<img src="docs/images/overview-dark.png" width="49%" alt="概览(深色)"> [![Downloads](https://img.shields.io/github/downloads/gentpan/OpenStats/total?color=6ee02b&label=downloads)](https://github.com/gentpan/OpenStats/releases)
<img src="docs/images/overview-light.png" width="49%" alt="概览(浅色)"> [![Stars](https://img.shields.io/github/stars/gentpan/OpenStats?style=flat&color=f5c518&label=stars)](https://github.com/gentpan/OpenStats/stargazers)
</p> [![Last commit](https://img.shields.io/github/last-commit/gentpan/OpenStats?color=black&label=last%20commit)](https://github.com/gentpan/OpenStats/commits/main)
[![Commit activity](https://img.shields.io/github/commit-activity/m/gentpan/OpenStats?color=black&label=commits)](https://github.com/gentpan/OpenStats/graphs/commit-activity)
[![CI](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml/badge.svg)](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml)
[![macOS](https://img.shields.io/badge/macOS-14%2B-black)](https://github.com/gentpan/OpenStats/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-black)](LICENSE)
OpenStats is a macOS menu-bar app that shows what your Mac is doing right now — per-core
CPU load, GPU, memory pressure, network speed, disk, battery, temperatures and fans — and
lets you act on it: spin the fans up, keep the Mac awake with the lid closed, clear caches.
Everything is read on your own Mac. No account, no telemetry, no network access at all.
[Download](https://github.com/gentpan/OpenStats/releases/latest) ·
[Website](https://getopenstats.com) ·
[Changelog](CHANGELOG.md) ·
[Architecture](ARCHITECTURE.md)
**English** · [简体中文](README.zh-CN.md)
</div>
--- ---
## 为什么做 OpenStats ## Install
市面上的菜单栏监控要么功能单一,要么常驻耗电。OpenStats 的目标很简单: OpenStats has not had a release yet. The first one will be a `.dmg` on
[Releases](https://github.com/gentpan/OpenStats/releases/latest), signed with a Developer ID
certificate and notarized by Apple, followed by a Homebrew cask. Until then, build it from
source — see [Build & run](#build--run).
- **一眼看全**:点开菜单栏就是一整屏仪表盘,不用翻页、不用下拉。 Requires macOS 14 (Sonoma) or later; the panel uses Liquid Glass on macOS 26 and later.
- **足够省电**:面板收起时只采集菜单栏用到的指标,M5 Max 实测 **约 0.3% CPU、16 MB 内存**。 Developed and tested on Apple Silicon. The interface is in Simplified Chinese.
- **原生体验**:Swift 6 + AppKit + SwiftUI,macOS 26 起使用系统液态玻璃,跟随浅色 / 深色外观。
## 功能 ## Recent updates
### 菜单栏 <!-- changelog:start -->
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
<p><img src="docs/images/menubar-dark.png" width="600" alt="菜单栏"></p> Not released yet · **23** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
| 指标 | 显示方式 | <details open>
|---|---| <summary><b>2026-09-13</b> · Unreleased · 15 added · 4 style · 4 fixed</summary>
| CPU / GPU / 内存 | 数字、柱状图、圆环、饼图、柱状图 + 数字,逐项可选 |
| 网速 | 两行显示:绿点上传、蓝点下载,完整单位(KB/s、MB/s、GB/s) |
| CPU 温度 / 风扇转速 | 迷你数值 |
| 防休眠 | 开启时显示咖啡杯图标 |
高负载时可选数值变红提醒;数值等宽右对齐,刷新时菜单栏不抖动。 **Added**
### 面板 - 菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。CPU、GPU、内存可以分别选择数字、柱状图、圆环、饼图、柱状图加数字五种样式;网速两行显示,绿点上传、蓝点下载,带完整单位。
- 下拉面板分为概览、进程、散热、防休眠、清理五页。概览页顶部是健康评分和芯片、内存、系统版本、运行时长、机型徽章,下面是 CPU 柱状历史、GPU 折线、内存面积图、磁盘、网络双线、风扇三列卡片,以及核心负载、电池环形图、高占用进程和快捷开关。
- CPU 按超级核、性能核、能效核分组显示各核心负载;内存口径与活动监视器一致,并显示内存压力;网速读取 64 位计数器,大流量下不会回绕。
- 温度与风扇:启动时枚举一次 SMC 键并按前缀归类,不按芯片型号硬编码,显示 CPU、GPU、内存、电池和掌托温度。
- 按界面需要分级采样:面板收起时只采集菜单栏用到的指标,锁屏、屏幕休眠和系统睡眠时暂停。
- 风扇调速:自动、降温、强冷、自定义四种模式。自定义模式下 CPU 达到安全温度会自动交还系统控制,应用退出或断开连接时风扇恢复自动。
- 防休眠:屏幕保持常亮、仅系统不休眠、合盖后继续运行,可设定持续时间;使用电池且电量低于下限时自动关闭合盖运行。
- 辅助工具:通过 SMAppService 注册,只提供设置风扇转速、切换合盖不睡眠、刷新 DNS、释放内存几个固定操作,并校验调用方签名;异常退出后下次开机自动恢复风扇与睡眠设置。
- 清理:扫描应用缓存、日志与崩溃报告、Chrome / Edge / Brave / Arc / Firefox / Safari 缓存、Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。按类别显示大小,可展开查看具体项目;清理前二次确认,操作写入 `~/Library/Logs/OpenStats/cleanup.log`。
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏显示项与样式、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 外观可选跟随系统、浅色、深色;macOS 26 起面板使用液态玻璃。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染面板、设置和菜单栏的浅色与深色截图。
- **概览仪表盘**:健康评分、芯片 / 内存 / 系统 / 运行时长徽章,CPU 柱状历史、GPU 折线、内存面积图、磁盘、网络双线、风扇、各核心负载(超级核 / 性能核 / 能效核分组)、电池环形图、高占用进程、快捷开关。 **Style**
- **进程**:按 CPU 或内存排序,辅助进程归并显示为所属应用。
- **散热**:CPU / GPU / 内存 / 电池 / 掌托温度;风扇实时转速与调速。 - 菜单栏字号对齐常见菜单栏监控工具:标签 7pt 细体、数值 12pt 常规、网速 9pt 细体,指标之间留出更宽的间距。
- **防休眠**:屏幕常亮、仅系统不休眠、**合盖后继续运行**,支持定时与低电量自动关闭。 - 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
- 更换应用图标,面板左上角和关于页使用同一图标。
- 首次启动默认在菜单栏显示 CPU、内存与网速。
**Fixed**
- 深色菜单栏上 CPU、RAM 小标签颜色过深,几乎看不清。
- 面板打开后高度会再调整一次、内容轻微回弹,看起来在抖动;面板打开时点击菜单栏图标关闭,会立刻又被打开。
- 风扇被其他程序设为手动模式时,误显示为由 OpenStats 控制。
- 风扇转速、PID 等数字出现千分位分隔符,例如 1,350 RPM。
</details>
<!-- changelog:end -->
## Activity
<p align="center"> <p align="center">
<img src="docs/images/thermal-dark.png" width="49%" alt="散热"> <img src="Assets/readme/activity.svg" alt="Commits per day over the last 26 weeks" width="760">
<img src="docs/images/keepawake-light.png" width="49%" alt="防休眠">
</p> </p>
### 风扇调速 <p align="center">
<a href="https://star-history.com/#gentpan/OpenStats&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=gentpan/OpenStats&type=Date&theme=dark">
<img alt="Star history" src="https://api.star-history.com/svg?repos=gentpan/OpenStats&type=Date" width="760">
</picture>
</a>
</p>
| 模式 | 行为 | ## At a glance
<p align="center">
<img src="Assets/readme/overview-dark.png" width="49%" alt="Overview, dark">
<img src="Assets/readme/overview-light.png" width="49%" alt="Overview, light">
</p>
## Where the numbers show
**Menu bar**
- CPU, GPU and memory each in one of five styles — number, bars, ring, pie, or bars with a
number — plus network speed, CPU temperature and the fastest fan.
- Network speed takes two rows: a green dot for upload, a blue dot for download, always with
its unit (`KB/s`, `MB/s`, `GB/s`).
- Labels at 7 pt and figures at 12 pt, matching the menu-bar monitors people already use.
Figures are fixed-width, so the bar does not jitter as they change.
<p align="center"><img src="Assets/readme/menubar-dark.png" width="600" alt="Menu bar"></p>
**Panel** — click the menu-bar item.
- **Overview.** A health score with badges for the chip, memory, macOS version, uptime and
model; then CPU history as bars, GPU as a line, memory, disk, network and fans in three
columns; per-core load grouped by core type (super, performance, efficiency); battery as a
ring; the busiest processes and quick switches.
- **Processes.** Sorted by CPU or memory; helper processes are shown under their app.
- **Thermal.** CPU, GPU, memory, battery and palm-rest temperatures, and the fans.
- **Keep awake.** Keep the display on, keep only the system awake, or keep running with the
lid closed — for a set time or indefinitely.
- **Clean.** Scan, review, clean. See [Cleanup](#cleanup).
The panel sizes itself to its content and needs no scrolling on common screens. `Esc`
closes it. Light, dark or follow the system.
<p align="center">
<img src="Assets/readme/thermal-dark.png" width="49%" alt="Thermal">
<img src="Assets/readme/keepawake-light.png" width="49%" alt="Keep awake">
</p>
## Fans and sleep
| Fan mode | What it does |
|---|---| |---|---|
| 自动 | 交还 macOS 温控 | | Auto | Hands the fans back to macOS |
| 降温 | 固定在最低到最高转速的 60% | | Cool | Holds them at 60% between minimum and maximum speed |
| 强冷 | 最高转速 | | Max | Full speed |
| 自定义 | 滑块自由设定 | | Custom | A slider, anywhere in the fan's range |
安全机制:自定义模式下 CPU 达到设定温度(默认 95°C)自动交还系统控制;应用退出、崩溃或断开连接时风扇自动恢复。 In Custom, the fans are handed back to macOS the moment the CPU reaches the safety
temperature (95 °C by default). Quitting OpenStats, or the app crashing, restores automatic
control. Running with the lid closed switches itself off on battery below a floor you choose.
### 清理与系统维护 Both need system privileges, provided by a small helper registered with `SMAppService` and
approved once in System Settings → General → Login Items. The helper offers a fixed set of
operations — set a fan's target speed, return fans to auto, toggle `pmset disablesleep`,
flush the DNS cache, purge memory — and **never runs arbitrary commands**. It checks the
caller's code signature, restores fans and sleep when the app disconnects, and after an
unclean exit restores them at the next boot.
- **一键扫描**:应用缓存、日志与崩溃报告、浏览器缓存(Chrome / Edge / Brave / Arc / Firefox / Safari)、Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包、废纸篓。 ## Cleanup
- **先预览再清理**:按类别显示大小,可展开查看具体项目并在访达中定位;清理前二次确认。
- **安全边界**:只清理白名单目录;钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过;执行前逐项重新校验;所有操作写入 `~/Library/Logs/OpenStats/cleanup.log`。
- **系统维护**:一键刷新 DNS 缓存、释放内存(已安装辅助工具时直接执行,否则请求一次管理员授权)。
<p><img src="docs/images/cleaner-light.png" width="600" alt="清理"></p> - **What it scans.** App caches, logs and crash reports, browser caches (Chrome, Edge,
Brave, Arc, Firefox, Safari), Xcode DerivedData, simulator caches, the npm cache, Xcode
archives, unfinished downloads, installers and the Trash.
- **Review first.** Sizes by category, every rule expandable to its items, a second click to
confirm. Caches and logs are deleted outright so the space comes back at once; downloads go
to the Trash. One switch sends everything to the Trash instead.
- **Safety.** Only allow-listed folders are ever touched. Keychains, password managers,
VPNs, cookies and history are off limits. Caches of running apps are skipped, browsers must
be quit first, and every item is checked again right before it goes. Each action is logged
to `~/Library/Logs/OpenStats/cleanup.log`.
- **Maintenance.** Flush the DNS cache and purge memory — through the helper if it is
installed, otherwise after a one-time administrator prompt.
### 设置 <p align="center"><img src="Assets/readme/cleaner-light.png" width="600" alt="Cleanup"></p>
<p><img src="docs/images/settings-menubar-light.png" width="600" alt="设置"></p> ## Your data
外观(跟随系统 / 浅色 / 深色)、登录时启动、刷新频率、温度单位、菜单栏显示项与样式、风扇安全温度、合盖模式电量下限。 OpenStats makes **no network connections**. Metrics come from the kernel (`host_processor_info`,
`host_statistics64`, `sysctl`), IOKit and the SMC on your own Mac. Preferences live in the
app's user defaults and contain nothing personal. There is no analytics and no telemetry.
## 技术实现 ## Build & run
| 数据 | 来源 | Requires macOS 14+, a **full Xcode** (the SwiftUI macro plugins do not ship with
|---|---| CommandLineTools) and [XcodeGen](https://github.com/yonaskolb/XcodeGen).
| CPU | `host_processor_info`,按 `hw.perflevelN` 区分核心类型 |
| 内存 | `host_statistics64`,口径与活动监视器一致;`kern.memorystatus_vm_pressure_level` |
| 网络 | `sysctl(NET_RT_IFLIST2)` 读取 64 位计数器(避免 `getifaddrs` 32 位回绕) |
| GPU | `IOAccelerator` 的 `PerformanceStatistics` |
| 温度 / 风扇 | AppleSMC;启动时枚举全部 SMC 键按前缀归类,**不硬编码芯片型号** |
| 进程 | `proc_pid_rusage`,CPU 时间与墙钟时间同用 mach 时间单位 |
| 电池 | `IOPowerSources` + `AppleSmartBattery` |
采集按界面需要分级:面板收起时只采菜单栏用到的指标;锁屏、屏幕休眠、系统睡眠时暂停。图表全部用 Canvas 自绘,面板关闭后立即销毁视图。
## 辅助工具与安全
风扇调速、“合盖后继续运行”、刷新 DNS 与释放内存需要系统权限,由 `SMAppService.daemon` 注册的辅助工具完成,首次使用时在“系统设置 › 通用 › 登录项”中批准一次。
- 只暴露固定的几个操作:设置风扇目标转速、恢复自动、切换 `pmset disablesleep`、刷新 DNS、释放内存,**不提供任意命令执行**。
- 通过 XPC 通信,并校验调用方代码签名。
- 客户端断开时自动恢复风扇与睡眠设置;异常退出后下次开机也会恢复。
## 安装
尚未发布正式版本。发布后将提供经过 Developer ID 签名与 Apple 公证的 DMG。
## 从源码构建
需要 Xcode 16 及以上,以及 [XcodeGen](https://github.com/yonaskolb/XcodeGen):
```bash ```bash
brew install xcodegen brew install xcodegen
make run # 生成工程、编译并启动(Debug) make run # generate the project, build Debug and launch
make run CONFIG=Release # Release 构建 make install # build Release, install into /Applications and launch
make install # Release 构建并安装到 /Applications 后启动 make test # unit tests: metrics, SMC decoding, cleanup safety
make test # 单元测试 make open # generate the project and open it in Xcode
make snapshot # 用本机实时数据渲染各页面截图到 build/snapshots
make open # 生成并用 Xcode 打开工程
``` ```
开发调试参数: The app is a menu-bar agent, so there is little to screenshot. Render the surfaces with
live data instead:
- `--snapshot <目录>`:输出面板、设置页、菜单栏的浅色 / 深色截图 ```bash
- `--show-panel`:启动后展开并固定面板,用于测量面板打开时的资源占用 build/DerivedData/Build/Products/Debug/OpenStats.app/Contents/MacOS/OpenStats --snapshot ./snapshots
## 项目结构
```
App/ 应用入口与图标
Helper/ root 辅助工具及其 launchd 配置
Packages/OpenStatsKit/
Sources/SMC SMC 读写、风扇控制、温度传感器归类
Sources/Metrics 指标采集与分级调度(MetricsHub)
Sources/HelperShared XPC 协议与系统维护命令
Sources/Cleaner 清理规则、安全守卫、扫描与执行引擎
Sources/OpenStatsUI 设计 token、面板、设置窗口、菜单栏绘制
Tests/ 单元测试(采集、SMC 编解码、清理安全边界)
project.yml XcodeGen 工程描述
docs/PLAN.md 技术方案与调研记录
``` ```
## 路线图 Glass and vibrancy exist only on screen. To measure the panel while it is open, launch with
`--show-panel`, which opens the panel and pins it.
- [x] 菜单栏多指标与多种显示样式 After editing `CHANGELOG.md`, run `python3 Scripts/sync_changelog.py` to refresh the recent
- [x] 概览仪表盘、进程、散热、防休眠 updates above and the activity chart.
- [x] 液态玻璃面板与浅色 / 深色主题
- [x] 风扇调速与合盖运行(辅助工具)
- [x] 缓存清理、刷新 DNS、释放内存
- [ ] 应用卸载(含残留文件与程序坞图标)
- [ ] 磁盘健康(SSD 寿命、写入量、温度)
- [ ] 点击菜单栏各指标弹出独立详情
- [ ] Developer ID 签名、公证与 DMG 发布
- [ ] Sparkle 自动更新
- [ ] 功耗、磁盘读写等更多指标
- [ ] 桌面小组件
## 致谢 ## Distribution
SMC 通信与 Apple Silicon 风扇控制流程移植自 [exelban/stats](https://github.com/exelban/stats)(MIT License),详见 [ThirdPartyNotices.md](ThirdPartyNotices.md)。 Developer ID only, not the App Store — the sandbox forbids talking to the SMC, reading other
apps' caches and installing a privileged helper, which is most of the feature set.
## 许可证 | What you have | What others get |
|---|---|
| Nothing | Ad-hoc signature — runs on your Mac only. Others see *"OpenStats is damaged"*. |
| Developer ID certificate | Hardened runtime. Others see *"Apple cannot check it for malicious software"*. |
| Certificate + notarization | Gatekeeper accepts it — the normal *"downloaded from the internet"* prompt. |
待定。 Before a release, set the Team ID in `HelperShared/HelperProtocol.swift` so the helper
requires the caller to be signed by it.
## Architecture
- `Packages/OpenStatsKit/Sources/SMC` — SMC reads and writes, fan control, temperature
sensor discovery.
- `Packages/OpenStatsKit/Sources/Metrics` — samplers for CPU, memory, network, GPU, disk,
battery, processes and sensors, and `MetricsHub`, which samples only what is on screen.
- `Packages/OpenStatsKit/Sources/Cleaner` — cleanup rules, the safety guard, scanning and
cleaning.
- `Packages/OpenStatsKit/Sources/HelperShared` — the XPC protocol and maintenance commands.
- `Packages/OpenStatsKit/Sources/OpenStatsUI` — design tokens, the panel, settings and the
menu-bar renderer.
- `Helper/` — the privileged helper and its launchd plist. `App/` — the entry point.
More in [ARCHITECTURE.md](ARCHITECTURE.md). Every change to the app is logged, dated, in
[CHANGELOG.md](CHANGELOG.md).
## Acknowledgements
OpenStats builds on these open-source projects. Thank you.
| Project | Author | License | What OpenStats took |
|---|---|---|---|
| [Stats](https://github.com/exelban/stats) | Serhiy Mytrovtsiy | MIT | SMC access, the Apple Silicon fan unlock sequence and the menu-bar mini widget metrics |
| [Mole](https://github.com/tw93/Mole) | tw93 | GPL-3.0 | Which folders are worth cleaning and which must never be touched; the cleaner is an independent Swift implementation and contains no Mole code |
| [QuotaBar](https://github.com/gentpan/quotabar) | GiantAccel, LLC | MIT | This README's layout, the changelog sync and the activity chart |
Details in [ThirdPartyNotices.md](ThirdPartyNotices.md).
OpenStats is an independent third-party app. It is not affiliated with, endorsed by, or
sponsored by Apple or any other company it mentions. Their names and logos belong to their
respective owners.
## License
MIT — see [LICENSE](LICENSE).
+233
View File
@@ -0,0 +1,233 @@
<div align="center">
<img src="Assets/icon.png" alt="OpenStats" width="112" height="112">
# OpenStats
**Mac 的状态,抬眼就看见——CPU、GPU、内存、网络与温度常驻菜单栏,还能调风扇、防休眠、一键清理。**
[![Release](https://img.shields.io/github/v/release/gentpan/OpenStats?color=6ee02b&label=%E7%89%88%E6%9C%AC)](https://github.com/gentpan/OpenStats/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/gentpan/OpenStats/total?color=6ee02b&label=%E4%B8%8B%E8%BD%BD)](https://github.com/gentpan/OpenStats/releases)
[![Stars](https://img.shields.io/github/stars/gentpan/OpenStats?style=flat&color=f5c518&label=%E6%98%9F%E6%A0%87)](https://github.com/gentpan/OpenStats/stargazers)
[![Last commit](https://img.shields.io/github/last-commit/gentpan/OpenStats?color=black&label=%E6%9C%80%E8%BF%91%E6%8F%90%E4%BA%A4)](https://github.com/gentpan/OpenStats/commits/main)
[![Commit activity](https://img.shields.io/github/commit-activity/m/gentpan/OpenStats?color=black&label=%E6%8F%90%E4%BA%A4)](https://github.com/gentpan/OpenStats/graphs/commit-activity)
[![CI](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml/badge.svg)](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml)
[![macOS](https://img.shields.io/badge/macOS-14%2B-black)](https://github.com/gentpan/OpenStats/releases/latest)
[![License](https://img.shields.io/badge/license-MIT-black)](LICENSE)
OpenStats 是一款 macOS 菜单栏应用,实时显示 Mac 正在做什么:各核心 CPU 负载、GPU、内存压力、
网速、磁盘、电池、温度和风扇,并且可以直接处理:给风扇提速、合盖后继续运行、清理缓存。
所有数据都在你自己的 Mac 上读取。无需注册账号,没有统计上报,完全不联网。
[下载](https://github.com/gentpan/OpenStats/releases/latest) ·
[官网](https://getopenstats.com) ·
[更新日志](CHANGELOG.md) ·
[架构说明](ARCHITECTURE.md)
[English](README.md) · **简体中文**
</div>
---
## 安装
OpenStats 还没有发布正式版本。第一个版本会在 [Releases](https://github.com/gentpan/OpenStats/releases/latest)
提供 `.dmg`,使用 Developer ID 证书签名并经过 Apple 公证,之后提供 Homebrew cask。
在此之前请从源码构建,见[构建与运行](#构建与运行)。
需要 macOS 14(Sonoma)或更高版本;macOS 26 起面板使用液态玻璃。在 Apple Silicon 上开发和测试。
界面为简体中文。
## 最近更新
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
尚未发布正式版本 · 开发中 **23** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-13</b> · 未发布 · 新增 15 · 样式 4 · 修复 4</summary>
**新增**
- 菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。CPU、GPU、内存可以分别选择数字、柱状图、圆环、饼图、柱状图加数字五种样式;网速两行显示,绿点上传、蓝点下载,带完整单位。
- 下拉面板分为概览、进程、散热、防休眠、清理五页。概览页顶部是健康评分和芯片、内存、系统版本、运行时长、机型徽章,下面是 CPU 柱状历史、GPU 折线、内存面积图、磁盘、网络双线、风扇三列卡片,以及核心负载、电池环形图、高占用进程和快捷开关。
- CPU 按超级核、性能核、能效核分组显示各核心负载;内存口径与活动监视器一致,并显示内存压力;网速读取 64 位计数器,大流量下不会回绕。
- 温度与风扇:启动时枚举一次 SMC 键并按前缀归类,不按芯片型号硬编码,显示 CPU、GPU、内存、电池和掌托温度。
- 按界面需要分级采样:面板收起时只采集菜单栏用到的指标,锁屏、屏幕休眠和系统睡眠时暂停。
- 风扇调速:自动、降温、强冷、自定义四种模式。自定义模式下 CPU 达到安全温度会自动交还系统控制,应用退出或断开连接时风扇恢复自动。
- 防休眠:屏幕保持常亮、仅系统不休眠、合盖后继续运行,可设定持续时间;使用电池且电量低于下限时自动关闭合盖运行。
- 辅助工具:通过 SMAppService 注册,只提供设置风扇转速、切换合盖不睡眠、刷新 DNS、释放内存几个固定操作,并校验调用方签名;异常退出后下次开机自动恢复风扇与睡眠设置。
- 清理:扫描应用缓存、日志与崩溃报告、Chrome / Edge / Brave / Arc / Firefox / Safari 缓存、Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。按类别显示大小,可展开查看具体项目;清理前二次确认,操作写入 `~/Library/Logs/OpenStats/cleanup.log`。
- 清理的安全边界:只清理白名单目录,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过,执行前逐项重新校验。
- 系统维护:一键刷新 DNS 缓存、释放内存,已安装辅助工具时直接执行,否则请求一次管理员授权。
- 进程页按 CPU 或内存排序,辅助进程归并显示为所属应用。
- 设置窗口:外观、登录时启动、刷新频率、温度单位、菜单栏显示项与样式、风扇安全温度、合盖运行电量下限、辅助工具安装与卸载。
- 外观可选跟随系统、浅色、深色;macOS 26 起面板使用液态玻璃。
- 截图命令 `OpenStats --snapshot <目录>`:用本机实时数据渲染面板、设置和菜单栏的浅色与深色截图。
**样式**
- 菜单栏字号对齐常见菜单栏监控工具:标签 7pt 细体、数值 12pt 常规、网速 9pt 细体,指标之间留出更宽的间距。
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
- 更换应用图标,面板左上角和关于页使用同一图标。
- 首次启动默认在菜单栏显示 CPU、内存与网速。
**修复**
- 深色菜单栏上 CPU、RAM 小标签颜色过深,几乎看不清。
- 面板打开后高度会再调整一次、内容轻微回弹,看起来在抖动;面板打开时点击菜单栏图标关闭,会立刻又被打开。
- 风扇被其他程序设为手动模式时,误显示为由 OpenStats 控制。
- 风扇转速、PID 等数字出现千分位分隔符,例如 1,350 RPM。
</details>
<!-- changelog:end -->
## 活跃度
<p align="center">
<img src="Assets/readme/activity.zh.svg" alt="近 26 周每天的提交数" width="760">
</p>
<p align="center">
<a href="https://star-history.com/#gentpan/OpenStats&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=gentpan/OpenStats&type=Date&theme=dark">
<img alt="星标历史" src="https://api.star-history.com/svg?repos=gentpan/OpenStats&type=Date" width="760">
</picture>
</a>
</p>
## 界面一览
<p align="center">
<img src="Assets/readme/overview-dark.png" width="49%" alt="概览(深色)">
<img src="Assets/readme/overview-light.png" width="49%" alt="概览(浅色)">
</p>
## 数据显示在哪里
**菜单栏**
- CPU、GPU、内存可以分别选择数字、柱状图、圆环、饼图、柱状图加数字五种样式,另外还能显示网速、
CPU 温度和转速最高的风扇。
- 网速两行显示:绿点上传、蓝点下载,始终带单位(`KB/s`、`MB/s`、`GB/s`)。
- 标签 7pt、数值 12pt,与常见的菜单栏监控工具一致;数值等宽,刷新时菜单栏不抖动。
<p align="center"><img src="Assets/readme/menubar-dark.png" width="600" alt="菜单栏"></p>
**面板**——点击菜单栏图标打开。
- **概览**:健康评分,芯片、内存、系统版本、运行时长、机型徽章;CPU 柱状历史、GPU 折线、内存、
磁盘、网络、风扇三列卡片;按超级核、性能核、能效核分组的核心负载;电池环形图;高占用进程和快捷开关。
- **进程**:按 CPU 或内存排序,辅助进程归并到所属应用。
- **散热**:CPU、GPU、内存、电池、掌托温度,以及风扇。
- **防休眠**:屏幕保持常亮、仅系统不休眠、合盖后继续运行,可定时或不限时。
- **清理**:扫描、预览、清理,见[清理](#清理)。
面板高度按内容自动计算,常见屏幕上不需要滚动;按 `Esc` 关闭。外观可选浅色、深色或跟随系统。
<p align="center">
<img src="Assets/readme/thermal-dark.png" width="49%" alt="散热">
<img src="Assets/readme/keepawake-light.png" width="49%" alt="防休眠">
</p>
## 风扇与睡眠
| 风扇模式 | 行为 |
|---|---|
| 自动 | 交还 macOS 温控 |
| 降温 | 固定在最低到最高转速之间的 60% |
| 强冷 | 最高转速 |
| 自定义 | 用滑块在风扇转速范围内自由设定 |
自定义模式下,CPU 一旦达到安全温度(默认 95°C)就交还系统控制。退出 OpenStats 或应用崩溃时,
风扇恢复自动。合盖运行在使用电池且电量低于你设定的下限时自动关闭。
这两项需要系统权限,由一个通过 `SMAppService` 注册的小型辅助工具完成,首次使用时在
“系统设置 › 通用 › 登录项”中批准一次。辅助工具只提供固定的几个操作:设置风扇目标转速、恢复自动、
切换 `pmset disablesleep`、刷新 DNS 缓存、释放内存,**不执行任意命令**。它会校验调用方的代码签名,
应用断开连接时恢复风扇与睡眠设置;异常退出后,下次开机也会恢复。
## 清理
- **扫描范围**:应用缓存、日志与崩溃报告、浏览器缓存(Chrome、Edge、Brave、Arc、Firefox、Safari)、
Xcode 编译缓存、模拟器缓存、npm 缓存、Xcode 归档、未完成的下载、安装包和废纸篓。
- **先预览**:按类别显示大小,每条规则可展开查看具体项目,清理前再确认一次。缓存与日志直接删除,
空间立即释放;下载目录的内容移到废纸篓。也可以一键改为全部先移到废纸篓。
- **安全**:只处理白名单目录。钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰。正在运行的应用的缓存
会跳过,浏览器需要先退出,删除前每一项都会再校验一次。所有操作记录在 `~/Library/Logs/OpenStats/cleanup.log`。
- **系统维护**:刷新 DNS 缓存、释放内存。已安装辅助工具时直接执行,否则请求一次管理员授权。
<p align="center"><img src="Assets/readme/cleaner-light.png" width="600" alt="清理"></p>
## 你的数据
OpenStats **不发起任何网络连接**。指标来自本机的内核接口(`host_processor_info`、`host_statistics64`、
`sysctl`)、IOKit 与 SMC。偏好设置保存在应用自己的 user defaults 中,不含个人信息。没有任何统计与上报。
## 构建与运行
需要 macOS 14+、**完整的 Xcode**(CommandLineTools 不含 SwiftUI 宏插件)以及
[XcodeGen](https://github.com/yonaskolb/XcodeGen)。
```bash
brew install xcodegen
make run # 生成工程,编译 Debug 并启动
make install # 编译 Release,安装到 /Applications 并启动
make test # 单元测试:指标采集、SMC 编解码、清理安全边界
make open # 生成工程并用 Xcode 打开
```
菜单栏应用没什么可截图的窗口,可以用本机实时数据渲染各个界面:
```bash
build/DerivedData/Build/Products/Debug/OpenStats.app/Contents/MacOS/OpenStats --snapshot ./snapshots
```
玻璃与半透明效果只在屏幕上存在。需要测量面板打开时的资源占用,用 `--show-panel` 启动,面板会自动展开并固定。
修改 `CHANGELOG.md` 后运行 `python3 Scripts/sync_changelog.py`,更新上方的“最近更新”和活跃度图。
## 分发
只走 Developer ID,不上 App Store——沙盒不允许访问 SMC、读取其他应用的缓存、安装特权辅助工具,
而这正是大部分功能。
| 你拥有的 | 别人打开时看到的 |
|---|---|
| 什么都没有 | ad-hoc 签名,只能在你自己的 Mac 上运行。别人会看到 *“OpenStats 已损坏”*。 |
| Developer ID 证书 | 启用 Hardened Runtime。别人会看到 *“Apple 无法检查其是否包含恶意软件”*。 |
| 证书 + 公证 | Gatekeeper 放行,只有常规的 *“从互联网下载”* 提示。 |
发布前请在 `HelperShared/HelperProtocol.swift` 中填写 Team ID,让辅助工具只接受由该团队签名的调用方。
## 架构
- `Packages/OpenStatsKit/Sources/SMC`——SMC 读写、风扇控制、温度传感器发现。
- `Packages/OpenStatsKit/Sources/Metrics`——CPU、内存、网络、GPU、磁盘、电池、进程、传感器的采集器,
以及只采集屏幕上需要的指标的 `MetricsHub`。
- `Packages/OpenStatsKit/Sources/Cleaner`——清理规则、安全守卫、扫描与执行。
- `Packages/OpenStatsKit/Sources/HelperShared`——XPC 协议与系统维护命令。
- `Packages/OpenStatsKit/Sources/OpenStatsUI`——设计 token、面板、设置窗口、菜单栏绘制。
- `Helper/`——特权辅助工具及其 launchd 配置;`App/`——应用入口。
更多内容见 [ARCHITECTURE.md](ARCHITECTURE.md)。应用的每一项改动都按日期记录在 [CHANGELOG.md](CHANGELOG.md)。
## 致谢
OpenStats 基于以下开源项目,谢谢。
| 项目 | 作者 | 许可证 | OpenStats 借鉴了什么 |
|---|---|---|---|
| [Stats](https://github.com/exelban/stats) | Serhiy Mytrovtsiy | MIT | SMC 访问、Apple Silicon 风扇解锁流程、菜单栏迷你样式的字号参数 |
| [Mole](https://github.com/tw93/Mole) | tw93 | GPL-3.0 | 哪些目录值得清理、哪些绝对不能碰;清理模块为独立的 Swift 实现,不含 Mole 代码 |
| [QuotaBar](https://github.com/gentpan/quotabar) | GiantAccel, LLC | MIT | 本 README 的版式、更新日志同步与活跃度图脚本 |
详见 [ThirdPartyNotices.md](ThirdPartyNotices.md)。
OpenStats 是独立的第三方应用,与 Apple 及文中提到的其他公司没有隶属、认可或赞助关系。相关名称与标志归各自所有者所有。
## 许可证
MIT,见 [LICENSE](LICENSE)。
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""Puts CHANGELOG.md where people look, and draws the commit calendar.
python3 Scripts/sync_changelog.py
Rewrites, from CHANGELOG.md and `git log`:
- the "recent updates" block in README.md and README.zh-CN.md, between
<!-- changelog:start --> and <!-- changelog:end -->;
- Assets/readme/activity.svg and activity.zh.svg, 26 weeks of commits.
Run it after every CHANGELOG edit. Standard library only, so it runs on a
stock Mac and in CI. Adapted from QuotaBar's script of the same name.
"""
import argparse
import datetime
import math
import pathlib
import re
import subprocess
from collections import Counter
ROOT = pathlib.Path(__file__).resolve().parent.parent
RECENT_DAYS = 3
WEEKS = 26
KIND_EN = {"新增": "Added", "样式": "Style", "修复": "Fixed"}
# ── CHANGELOG.md ──────────────────────────────────────────────────────────
def parse(text):
"""Releases, newest first: each with its days, each day with its groups.
"## 未发布" holds "### YYYY-MM-DD" days; "## 0.1.0 · 2026-09-20" holds its
groups directly, which count as one day on the release date.
"""
releases, release, day, group = [], None, None, None
for line in text.splitlines():
heading = re.match(r"^## (.+)$", line)
dated = re.match(r"^### (\d{4}-\d{2}-\d{2})\s*$", line)
kind = re.match(r"^#### (.+)$", line)
if heading:
title = heading.group(1).strip()
version = re.match(r"^v?([\d.]+)\s*·\s*(\d{4}-\d{2}-\d{2})$", title)
release = {
"version": version.group(1) if version else None,
"date": version.group(2) if version else None,
"days": [],
}
releases.append(release)
day = {"date": release["date"], "groups": []} if version else None
if day:
release["days"].append(day)
group = None
elif dated and release is not None:
# A release that kept its dated days: drop the empty day that
# stood in for the release date.
if release["days"] and not release["days"][-1]["groups"]:
release["days"].pop()
day = {"date": dated.group(1), "groups": []}
release["days"].append(day)
group = None
elif kind and day is not None:
group = {"kind": kind.group(1).strip(), "items": []}
day["groups"].append(group)
elif line.startswith("- ") and group is not None:
group["items"].append(line[2:].strip())
elif line.startswith(" ") and line.strip() and group is not None and group["items"]:
group["items"][-1] += line.strip()
for release in releases:
release["days"] = [d for d in release["days"] if any(g["items"] for g in d["groups"])]
return [r for r in releases if r["days"]]
def day_entries(releases):
"""Every day, newest first, with the release it belongs to."""
days = [(release, day) for release in releases for day in release["days"]]
days.sort(key=lambda pair: pair[1]["date"] or "", reverse=True)
return days
def item_count(day):
return sum(len(g["items"]) for g in day["groups"])
def counts(day, en):
parts = []
for g in day["groups"]:
n = len(g["items"])
if en:
parts.append(f"{n} {KIND_EN.get(g['kind'], g['kind']).lower()}")
else:
parts.append(f"{g['kind']} {n}")
return " · ".join(parts)
def latest_release(releases):
return next((r for r in releases if r["version"]), None)
def unreleased(releases):
return next((r for r in releases if not r["version"]), None)
# ── README ────────────────────────────────────────────────────────────────
def readme_block(releases, en):
latest = latest_release(releases)
pending = unreleased(releases)
pending_count = sum(item_count(d) for d in pending["days"]) if pending else 0
lines = ["<!-- changelog:start -->"]
lines.append("<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->"
if en else "<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->")
head = []
if latest:
head.append(f"Latest release **{latest['version']}** ({latest['date']})" if en
else f"最新版本 **{latest['version']}**({latest['date']})")
elif en:
head.append("Not released yet")
else:
head.append("尚未发布正式版本")
if pending_count:
head.append(f"**{pending_count}** changes in development" if en
else f"开发中 **{pending_count}** 项改动尚未发布")
head.append("[full changelog](CHANGELOG.md) (kept in Chinese)" if en else "[完整更新日志](CHANGELOG.md)")
lines += ["", " · ".join(head), ""]
for index, (release, day) in enumerate(day_entries(releases)[:RECENT_DAYS]):
label = release["version"] or ("Unreleased" if en else "未发布")
opened = " open" if index == 0 else ""
lines.append(f"<details{opened}>")
lines.append(f"<summary><b>{day['date']}</b> · {label} · {counts(day, en)}</summary>")
lines.append("")
for g in day["groups"]:
lines.append(f"**{KIND_EN.get(g['kind'], g['kind']) if en else g['kind']}**")
lines.append("")
lines += [f"- {item}" for item in g["items"]]
lines.append("")
lines.append("</details>")
lines.append("")
lines.append("<!-- changelog:end -->")
return "\n".join(lines)
def replace_block(path, block, start="<!-- changelog:start -->", end="<!-- changelog:end -->"):
text = path.read_text(encoding="utf-8")
pattern = re.compile(re.escape(start) + r".*?" + re.escape(end), re.S)
if not pattern.search(text):
raise SystemExit(f"{path.relative_to(ROOT)}: no {start} … {end} markers")
updated = pattern.sub(lambda _: block, text, count=1)
if updated != text:
path.write_text(updated, encoding="utf-8")
return True
return False
# ── Commit calendar ───────────────────────────────────────────────────────
def commit_days():
dates = subprocess.run(
["git", "log", "--format=%ad", "--date=short"],
cwd=ROOT, capture_output=True, text=True, check=True).stdout.split()
return Counter(dates)
def activity_svg(per_day, today, en):
cell, gap, left, top = 11, 3, 30, 18
step = cell + gap
# Columns are weeks starting Sunday, the last one holding today.
end = today + datetime.timedelta(days=(5 - today.weekday()) % 7) # the Saturday ending this week
start = end - datetime.timedelta(days=WEEKS * 7 - 1)
days = [start + datetime.timedelta(days=i) for i in range(WEEKS * 7)]
window = {d: per_day.get(d.isoformat(), 0) for d in days if d <= today}
total = sum(window.values())
peak = max(window.values(), default=0)
palette = ["rgba(139,148,158,0.18)", "#9be9a8", "#40c463", "#30a14e", "#216e39"]
def level(n):
return 0 if n == 0 or peak == 0 else max(1, min(4, math.ceil(4 * n / peak)))
width = left + WEEKS * step
height = top + 7 * step + 40
font = 'font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,PingFang SC,sans-serif"'
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img">']
title = (f"{total} commits in the last {WEEKS} weeks" if en else f"近 {WEEKS} 周共 {total} 次提交")
parts.append(f"<title>{title}</title>")
parts.append(f'<g {font} font-size="9" fill="#8b949e">')
months_en = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
last_month = None
for w in range(WEEKS):
first = days[w * 7]
if first.month != last_month and first.day <= 7:
label = months_en[first.month - 1] if en else f"{first.month}月"
parts.append(f'<text x="{left + w * step}" y="{top - 6}">{label}</text>')
last_month = first.month
elif last_month is None:
last_month = first.month
for row, label in ((1, "Mon" if en else "一"), (3, "Wed" if en else "三"), (5, "Fri" if en else "五")):
parts.append(f'<text x="0" y="{top + row * step + cell - 2}">{label}</text>')
parts.append("</g>")
for i, d in enumerate(days):
if d > today:
continue
n = window[d]
x, y = left + (i // 7) * step, top + (i % 7) * step
tip = f"{n} commits on {d.isoformat()}" if en else f"{d.isoformat()}:{n} 次提交"
parts.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" rx="2" fill="{palette[level(n)]}"><title>{tip}</title></rect>')
# The legend under the grid on the right, the caption on its own line
# below: side by side they collide in English.
base = top + 7 * step + 14
caption = (f"{total} commits in the last {WEEKS} weeks · through {today.isoformat()}" if en
else f"近 {WEEKS} 周共 {total} 次提交 · 截至 {today.isoformat()}")
parts.append(f'<g {font} font-size="10" fill="#8b949e">')
parts.append(f'<text x="{left}" y="{base + 16}">{caption}</text>')
legend_x = width - 5 * step - 34
parts.append(f'<text x="{legend_x - 4}" y="{base}" text-anchor="end">{"Less" if en else "少"}</text>')
for k in range(5):
parts.append(f'<rect x="{legend_x + k * step}" y="{base - cell + 1}" width="{cell}" height="{cell}" rx="2" fill="{palette[k]}"/>')
parts.append(f'<text x="{legend_x + 5 * step + 2}" y="{base}">{"More" if en else "多"}</text>')
parts.append("</g></svg>")
return "\n".join(parts) + "\n"
def write_if_changed(path, content):
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and path.read_text(encoding="utf-8") == content:
return False
path.write_text(content, encoding="utf-8")
return True
def main():
parser = argparse.ArgumentParser(description="Sync CHANGELOG.md into the READMEs and the activity chart.")
parser.add_argument("--changelog", default=str(ROOT / "CHANGELOG.md"),
help="read this file instead, e.g. the committed copy while other edits are pending")
parser.add_argument("--through-today", action="store_true",
help="include today's commits in the calendar (default stops at yesterday)")
args = parser.parse_args()
releases = parse(pathlib.Path(args.changelog).read_text(encoding="utf-8"))
changed = []
if replace_block(ROOT / "README.md", readme_block(releases, en=True)):
changed.append("README.md")
if replace_block(ROOT / "README.zh-CN.md", readme_block(releases, en=False)):
changed.append("README.zh-CN.md")
# The calendar runs through yesterday by default: a finished day does not
# change, so running this again after today's commits leaves the chart
# alone rather than redrawing it with every commit that records the redraw.
today = datetime.date.today()
if not args.through_today:
today -= datetime.timedelta(days=1)
per_day = commit_days()
for name, en in (("activity.svg", True), ("activity.zh.svg", False)):
if write_if_changed(ROOT / "Assets" / "readme" / name, activity_svg(per_day, today, en)):
changed.append(f"Assets/readme/{name}")
print("已更新:" + "、".join(changed) if changed else "已是最新")
if __name__ == "__main__":
main()