README 改为与 QuotaBar 统一的版式
- 顶部居中 Logo、同配色徽章(版本、下载、星标、最近提交、提交频率、CI、macOS、许可证) - 中英文两份 README,最近更新由 CHANGELOG.md 生成,附提交活跃度热力图与星标历史 - 新增 CHANGELOG.md、ARCHITECTURE.md、MIT LICENSE、Scripts/sync_changelog.py、CI 工作流 - README 图片移到 Assets/readme
No files matched your search
@@ -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/
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 189 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
@@ -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。
|
||||
@@ -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.
|
||||
@@ -1,170 +1,260 @@
|
||||
<p align="center">
|
||||
<img src="App/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" width="112" alt="OpenStats 图标">
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
<h1 align="center">OpenStats</h1>
|
||||
<img src="Assets/icon.png" alt="OpenStats" width="112" height="112">
|
||||
|
||||
<p align="center">
|
||||
<b>轻量、原生的 macOS 菜单栏系统监控</b><br>
|
||||
CPU · GPU · 内存 · 网络 · 磁盘 · 电池 · 温度 · 风扇调速 · 防休眠<br>
|
||||
<a href="https://getopenstats.com">getopenstats.com</a>
|
||||
</p>
|
||||
# OpenStats
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
**Your Mac at a glance — CPU, GPU, memory, network and temperatures in the menu bar, with fan control, keep-awake and one-click cleanup.**
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/overview-dark.png" width="49%" alt="概览(深色)">
|
||||
<img src="docs/images/overview-light.png" width="49%" alt="概览(浅色)">
|
||||
</p>
|
||||
[](https://github.com/gentpan/OpenStats/releases/latest)
|
||||
[](https://github.com/gentpan/OpenStats/releases)
|
||||
[](https://github.com/gentpan/OpenStats/stargazers)
|
||||
[](https://github.com/gentpan/OpenStats/commits/main)
|
||||
[](https://github.com/gentpan/OpenStats/graphs/commit-activity)
|
||||
[](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml)
|
||||
[](https://github.com/gentpan/OpenStats/releases/latest)
|
||||
[](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).
|
||||
|
||||
- **一眼看全**:点开菜单栏就是一整屏仪表盘,不用翻页、不用下拉。
|
||||
- **足够省电**:面板收起时只采集菜单栏用到的指标,M5 Max 实测 **约 0.3% CPU、16 MB 内存**。
|
||||
- **原生体验**:Swift 6 + AppKit + SwiftUI,macOS 26 起使用系统液态玻璃,跟随浅色 / 深色外观。
|
||||
Requires macOS 14 (Sonoma) or later; the panel uses Liquid Glass on macOS 26 and later.
|
||||
Developed and tested on Apple Silicon. The interface is in Simplified Chinese.
|
||||
|
||||
## 功能
|
||||
## 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)
|
||||
|
||||
| 指标 | 显示方式 |
|
||||
|---|---|
|
||||
| CPU / GPU / 内存 | 数字、柱状图、圆环、饼图、柱状图 + 数字,逐项可选 |
|
||||
| 网速 | 两行显示:绿点上传、蓝点下载,完整单位(KB/s、MB/s、GB/s) |
|
||||
| CPU 温度 / 风扇转速 | 迷你数值 |
|
||||
| 防休眠 | 开启时显示咖啡杯图标 |
|
||||
<details open>
|
||||
<summary><b>2026-09-13</b> · Unreleased · 15 added · 4 style · 4 fixed</summary>
|
||||
|
||||
高负载时可选数值变红提醒;数值等宽右对齐,刷新时菜单栏不抖动。
|
||||
**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 折线、内存面积图、磁盘、网络双线、风扇、各核心负载(超级核 / 性能核 / 能效核分组)、电池环形图、高占用进程、快捷开关。
|
||||
- **进程**:按 CPU 或内存排序,辅助进程归并显示为所属应用。
|
||||
- **散热**:CPU / GPU / 内存 / 电池 / 掌托温度;风扇实时转速与调速。
|
||||
- **防休眠**:屏幕常亮、仅系统不休眠、**合盖后继续运行**,支持定时与低电量自动关闭。
|
||||
**Style**
|
||||
|
||||
- 菜单栏字号对齐常见菜单栏监控工具:标签 7pt 细体、数值 12pt 常规、网速 9pt 细体,指标之间留出更宽的间距。
|
||||
- 面板加宽为三列布局,高度按内容自动计算,常见屏幕上不需要滚动。
|
||||
- 更换应用图标,面板左上角和关于页使用同一图标。
|
||||
- 首次启动默认在菜单栏显示 CPU、内存与网速。
|
||||
|
||||
**Fixed**
|
||||
|
||||
- 深色菜单栏上 CPU、RAM 小标签颜色过深,几乎看不清。
|
||||
- 面板打开后高度会再调整一次、内容轻微回弹,看起来在抖动;面板打开时点击菜单栏图标关闭,会立刻又被打开。
|
||||
- 风扇被其他程序设为手动模式时,误显示为由 OpenStats 控制。
|
||||
- 风扇转速、PID 等数字出现千分位分隔符,例如 1,350 RPM。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- changelog:end -->
|
||||
|
||||
## Activity
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/thermal-dark.png" width="49%" alt="散热">
|
||||
<img src="docs/images/keepawake-light.png" width="49%" alt="防休眠">
|
||||
<img src="Assets/readme/activity.svg" alt="Commits per day over the last 26 weeks" 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="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 温控 |
|
||||
| 降温 | 固定在最低到最高转速的 60% |
|
||||
| 强冷 | 最高转速 |
|
||||
| 自定义 | 滑块自由设定 |
|
||||
| Auto | Hands the fans back to macOS |
|
||||
| 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 归档、未完成的下载、安装包、废纸篓。
|
||||
- **先预览再清理**:按类别显示大小,可展开查看具体项目并在访达中定位;清理前二次确认。
|
||||
- **安全边界**:只清理白名单目录;钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;正在运行的应用和浏览器自动跳过;执行前逐项重新校验;所有操作写入 `~/Library/Logs/OpenStats/cleanup.log`。
|
||||
- **系统维护**:一键刷新 DNS 缓存、释放内存(已安装辅助工具时直接执行,否则请求一次管理员授权)。
|
||||
## Cleanup
|
||||
|
||||
<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
|
||||
|
||||
| 数据 | 来源 |
|
||||
|---|---|
|
||||
| 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):
|
||||
Requires macOS 14+, a **full Xcode** (the SwiftUI macro plugins do not ship with
|
||||
CommandLineTools) and [XcodeGen](https://github.com/yonaskolb/XcodeGen).
|
||||
|
||||
```bash
|
||||
brew install xcodegen
|
||||
make run # 生成工程、编译并启动(Debug)
|
||||
make run CONFIG=Release # Release 构建
|
||||
make install # Release 构建并安装到 /Applications 后启动
|
||||
make test # 单元测试
|
||||
make snapshot # 用本机实时数据渲染各页面截图到 build/snapshots
|
||||
make open # 生成并用 Xcode 打开工程
|
||||
make run # generate the project, build Debug and launch
|
||||
make install # build Release, install into /Applications and launch
|
||||
make test # unit tests: metrics, SMC decoding, cleanup safety
|
||||
make open # generate the project and open it in Xcode
|
||||
```
|
||||
|
||||
开发调试参数:
|
||||
The app is a menu-bar agent, so there is little to screenshot. Render the surfaces with
|
||||
live data instead:
|
||||
|
||||
- `--snapshot <目录>`:输出面板、设置页、菜单栏的浅色 / 深色截图
|
||||
- `--show-panel`:启动后展开并固定面板,用于测量面板打开时的资源占用
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
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 技术方案与调研记录
|
||||
```bash
|
||||
build/DerivedData/Build/Products/Debug/OpenStats.app/Contents/MacOS/OpenStats --snapshot ./snapshots
|
||||
```
|
||||
|
||||
## 路线图
|
||||
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] 菜单栏多指标与多种显示样式
|
||||
- [x] 概览仪表盘、进程、散热、防休眠
|
||||
- [x] 液态玻璃面板与浅色 / 深色主题
|
||||
- [x] 风扇调速与合盖运行(辅助工具)
|
||||
- [x] 缓存清理、刷新 DNS、释放内存
|
||||
- [ ] 应用卸载(含残留文件与程序坞图标)
|
||||
- [ ] 磁盘健康(SSD 寿命、写入量、温度)
|
||||
- [ ] 点击菜单栏各指标弹出独立详情
|
||||
- [ ] Developer ID 签名、公证与 DMG 发布
|
||||
- [ ] Sparkle 自动更新
|
||||
- [ ] 功耗、磁盘读写等更多指标
|
||||
- [ ] 桌面小组件
|
||||
After editing `CHANGELOG.md`, run `python3 Scripts/sync_changelog.py` to refresh the recent
|
||||
updates above and the activity chart.
|
||||
|
||||
## 致谢
|
||||
## 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).
|
||||
@@ -0,0 +1,233 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="Assets/icon.png" alt="OpenStats" width="112" height="112">
|
||||
|
||||
# OpenStats
|
||||
|
||||
**Mac 的状态,抬眼就看见——CPU、GPU、内存、网络与温度常驻菜单栏,还能调风扇、防休眠、一键清理。**
|
||||
|
||||
[](https://github.com/gentpan/OpenStats/releases/latest)
|
||||
[](https://github.com/gentpan/OpenStats/releases)
|
||||
[](https://github.com/gentpan/OpenStats/stargazers)
|
||||
[](https://github.com/gentpan/OpenStats/commits/main)
|
||||
[](https://github.com/gentpan/OpenStats/graphs/commit-activity)
|
||||
[](https://github.com/gentpan/OpenStats/actions/workflows/ci.yml)
|
||||
[](https://github.com/gentpan/OpenStats/releases/latest)
|
||||
[](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)。
|
||||
@@ -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()
|
||||