Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
name: Build

name: Build and test
on:
push:
branches: [main]
branches: [main, 'codex/**']
pull_request:
branches: [main]

permissions:
contents: read
jobs:
build:
test:
runs-on: macos-15

timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Build
run: swift build
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
xcode-version: '26.3'
- run: python3 -m unittest discover -s Tests/Release -v
- run: swift test
- name: Build and verify Universal app
env:
ARCHS: arm64 x86_64
run: ./scripts/make-app.sh
- name: Check bundled resources and version
run: |
test -d .build/app/SMARTastic.app/Contents/Resources/SMARTastic_SMARTastic.bundle
./scripts/check-architectures.sh .build/app/SMARTastic.app/Contents/MacOS/SMARTastic
codesign --verify --deep --strict .build/app/SMARTastic.app
139 changes: 139 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Signed release and Homebrew
on:
workflow_dispatch:
inputs:
version:
description: 'Version (x.y.z)'
required: true
type: string
resume_run_id:
description: 'Optional original run ID for an interrupted notarization'
required: false
type: string
permissions:
contents: write
actions: read
concurrency:
group: smartastic-release
cancel-in-progress: false
jobs:
release:
runs-on: macos-15
timeout-minutes: 60
env:
VERSION: ${{ inputs.version }}
RELEASE_REPOSITORY: ${{ github.repository }}
NOTARY_PROFILE: smartastic-release
steps:
- name: Set temporary keychain paths
run: |
printf 'CODE_SIGN_KEYCHAIN=%s/smartastic-signing.keychain-db\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
printf 'NOTARY_KEYCHAIN=%s/smartastic-signing.keychain-db\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
xcode-version: '26.3'
- name: Check release and tap access
env:
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: |
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1
test -n "$GH_TOKEN"
test "$(gh api repos/localfoundry/homebrew-tap --jq .permissions.push)" = true
- name: Resolve release or restore notarization state
id: state
env:
GH_TOKEN: ${{ github.token }}
RESUME_RUN_ID: ${{ inputs.resume_run_id }}
run: python3 scripts/prepare-release.py
- name: Run tests
if: steps.state.outputs.mode == 'build'
run: |
python3 -m unittest discover -s Tests/Release -v
swift test
- name: Import signing certificate and notary credentials
if: steps.state.outputs.mode == 'build'
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
for variable in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
[[ -n "${!variable}" ]] || { echo "Required secret missing: $variable" >&2; exit 1; }
done
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Version must be x.y.z.' >&2; exit 1; }
keychain_password="$(openssl rand -hex 32)"
echo "::add-mask::$keychain_password"
printf '%s' "$CSC_LINK" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
security create-keychain -p "$keychain_password" "$CODE_SIGN_KEYCHAIN"
security set-keychain-settings -lut 3600 "$CODE_SIGN_KEYCHAIN"
security unlock-keychain -p "$keychain_password" "$CODE_SIGN_KEYCHAIN"
# codesign also searches for the private key and certificate chain here.
python3 - <<'PYTHON'
import os, shlex, subprocess
existing = shlex.split(subprocess.check_output(
["security", "list-keychains", "-d", "user"], text=True))
subprocess.run(["security", "list-keychains", "-d", "user", "-s",
os.environ["CODE_SIGN_KEYCHAIN"], *existing], check=True)
PYTHON
security import "$RUNNER_TEMP/certificate.p12" -P "$CSC_KEY_PASSWORD" -k "$CODE_SIGN_KEYCHAIN" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$CODE_SIGN_KEYCHAIN"
rm "$RUNNER_TEMP/certificate.p12"
# Use the sole valid Developer ID identity imported for this team.
python3 - <<'PYTHON'
import os, re, subprocess
result = subprocess.run(
["security", "find-identity", "-v", "-p", "codesigning", os.environ["CODE_SIGN_KEYCHAIN"]],
check=True, capture_output=True, text=True)
identities = re.findall(r'"(Developer ID Application: [^"\n]+)"', result.stdout)
identities = [name for name in identities if name.endswith("(" + os.environ["APPLE_TEAM_ID"] + ")")]
if len(identities) != 1:
raise SystemExit("Expected exactly one valid Developer ID Application identity for APPLE_TEAM_ID.")
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as output:
output.write("CODE_SIGN_IDENTITY=" + identities[0] + "\n")
PYTHON
xcrun notarytool store-credentials "$NOTARY_PROFILE" --keychain "$NOTARY_KEYCHAIN" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID"
- name: Build Universal app and notarize
if: steps.state.outputs.mode == 'build'
env:
ARCHS: arm64 x86_64
run: ./scripts/release.sh .build/releases
- name: Verify final or restored archive
run: ./scripts/verify-release.sh
- name: Publish verified release
if: steps.state.outputs.mode != 'tap'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v$VERSION" \
".build/releases/SMARTastic-$VERSION.zip" \
".build/releases/SMARTastic-$VERSION.zip.sha256" \
".build/releases/Casks/smartastic.rb" \
--target "$GITHUB_SHA" --title "SMARTastic $VERSION" --generate-notes
mkdir -p .build/download-check
gh release download "v$VERSION" --pattern "SMARTastic-$VERSION.zip*" --dir .build/download-check
(cd .build/download-check && shasum -a 256 -c "SMARTastic-$VERSION.zip.sha256")
- name: Update and verify Homebrew tap
env:
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: ./scripts/update-tap.sh
- name: Remove temporary signing material
if: always()
run: |
rm -f "$RUNNER_TEMP/certificate.p12"
if [[ -f "$CODE_SIGN_KEYCHAIN" ]]; then
security delete-keychain "$CODE_SIGN_KEYCHAIN"
fi
- name: Preserve non-secret notarization state
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: smartastic-notarization-${{ github.run_id }}
path: .build/releases
overwrite: true
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
.DS_Store
*.dSYM
*.dSYM/**

__pycache__/
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import PackageDescription

let package = Package(
name: "SMARTastic",
defaultLocalization: "de",
defaultLocalization: "en",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "SMARTastic",
resources: [.process("Resources")]
)
),
.testTarget(name: "SMARTasticTests", dependencies: ["SMARTastic"])
]
)
174 changes: 141 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,165 @@
<p align="center"><img src="assets/logo.png" alt="SMARTastic logo" width="128"></p>

# SMARTastic

Native macOS app for visualizing SSD and HDD SMART data.
A native macOS app for understanding the health of your SSDs and hard drives.
Built with SwiftUI, powered by [smartmontools](https://www.smartmontools.org/),
and developed by [Robin Bially](https://github.com/RobinBially).

<p align="center">
<img src="https://img.shields.io/badge/macOS-14%2B-blue?logo=apple" alt="macOS">
<img src="https://img.shields.io/badge/Swift-6-orange?logo=swift" alt="Swift">
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
<p>
<img src="https://img.shields.io/badge/macOS-14%2B-blue?logo=apple" alt="macOS 14 or later">
<img src="https://img.shields.io/badge/SwiftUI-native-orange?logo=swift" alt="Native SwiftUI app">
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license">
</p>

---
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/screenshot-dark.png">
<img src="assets/screenshot-light.png" alt="SMARTastic showing an NVMe SSD's health, temperature, remaining rated endurance and usage" width="1100">
</picture>

*Actual app screenshots, using clearly labelled synthetic demo data.*

## What it shows

- **Internal and external drives**, including Apple SSDs, NVMe drives and ATA disks.
- **Clear health states:** good, warning, critical or unknown. Failed SMART status,
NVMe critical warnings and depleted endurance are never hidden by a green score.
- **Available measurements:** temperature, rated endurance, spare capacity,
media/sector error counters, data read/written, power-on hours and power cycles.
- **Daily write history:** a native bar chart for 7, 30 or 90 days, with a today
summary and selectable bars. Uses real timestamps and counter differences,
independently of SMART power-on hours.
- **Drive search** by model or interface, with native keyboard selection and
Escape to clear the focused search field.
- **Refresh controls:** native segments for pause/manual, every 30 seconds,
every minute (default), or every five minutes. The last successful scan time stays visible.
- **JSON report export** with a versioned schema and measurement timestamp.
Serial-number fields are omitted; review diagnostic text before sharing.
- **Read diagnostics** with a full-width clickable disclosure row and useful guidance when smartmontools or SMART access is
unavailable. A failed scan preserves the previous snapshot and shows a warning.
- **Compact layout and native circular actions**, with content scrolling beneath
the transparent window header.
- **A sun/moon appearance switch**, with system appearance by default (right-click
the switch to restore it), plus English, German, French, Spanish and
Simplified Chinese translations selected from your system preferences.

<img src="assets/screenshot-warning.png" alt="SMARTastic showing an HDD with sector warnings and unavailable measurements displayed as dashes" width="1100">

## Daily write history

<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/history-dark.png">
<img src="assets/history-light.png" alt="Daily write history with a mint today bar and visible gaps, using synthetic demo data" width="900">
</picture>


History starts when this version first scans a drive. SMART only exposes a lifetime
counter; it cannot reconstruct earlier daily usage. Keep SMARTastic open with
periodic refresh enabled to collect regular measurements. There is no background
agent when the app is quit. ⌘W minimizes the window so measurement can continue;
⌘Q quits the app. The standard ⌘M shortcut also remains available.

The chart shows measured GB, not extrapolated full-day totals. Missing days stay
empty; an observed zero is shown as a dot. Today's value and other partially
observed days are incomplete. Select a bar to see its measured amount and covered
hours. Hover over the chart for a floating daily detail card, including explicit
missing-data messages. Click to pin a day; click it again to clear the selection.
Buttons and period controls include contextual tooltips. Short intervals crossing midnight (up to 10 minutes) are split in proportion
to elapsed time. Longer intervals crossing days are reported separately rather
than assigned to invented daily totals. Their entire volume is reported if the
interval overlaps the selected period, so it may include writes outside that period.

Up to 90 calendar days are kept locally in
`~/Library/Application Support/SMARTastic/write-history.json`. A hash of model and
serial identifies each drive across device-path changes; raw serial numbers are
not stored. Drives without a reliable serial or write counter cannot be tracked.
Counter decreases establish a fresh baseline. The calendar time zone is fixed when
the history is created and displayed under the chart's measurement explanation.
Demo history stays in memory and never enters the real history file. The existing
JSON report exports the current SMART snapshot, not this local history.

## Install

Requires **macOS 14 Sonoma or later** and **smartmontools 7 or later**.
The app supports Apple Silicon and Intel. Homebrew distribution uses the
[LocalFoundry tap](https://github.com/localfoundry/homebrew-tap).

Install the signed and Apple-notarized Universal app through Homebrew:

```sh
brew install --cask localfoundry/tap/smartastic
```

<p align="center">
<img src="assets/screenshot.png" alt="SMARTastic Screenshot" width="720">
</p>
The cask also installs smartmontools. For a manual app download from
[GitHub Releases](https://github.com/RobinBially/SMARTastic/releases), install
smartmontools separately:

```sh
brew install smartmontools
```

**SMARTastic** shows drive health at a glance — temperature, wear, errors, TB written, and estimated remaining life. Built with SwiftUI, powered by `smartctl`.
SMARTastic looks for smartctl at `/opt/homebrew/bin/smartctl` on Apple Silicon
and `/usr/local/bin/smartctl` on Intel. It does not ask for administrator access,
install a privileged helper, start disk self-tests, or change drive settings.

## Features
## Understanding the numbers

- **NVMe SSDs & ATA HDDs** — Full SMART data where available
- **Health gauges** — Temperature, spare, media errors, usage
- **Usage stats** — Data read/written, daily write rate, power-on hours
- **Life estimate** — Based on current wear rate
- **Auto-refresh** — Every 30 seconds
- **Color-coded** — Green/orange/red at a glance
SMART reports what a drive and its controller expose. USB adapters, RAID
controllers and access permissions can prevent some or all SMART data from being
read. The app still shows the available macOS drive information.

## Requirements
**A dash means unavailable, not zero.** ATA attribute meanings vary by vendor;
unsupported SSD wear and traffic counters are not guessed. For NVMe, one data
unit is 512,000 bytes, and TB/GB use decimal units. ATA error counts aggregate
reported reallocated, pending and offline-uncorrectable counters; categories can
overlap and are not a count of distinct failing sectors.

- macOS 14+
- `brew install smartmontools`
**Remaining rated endurance is not a lifespan prediction.** It is 100 minus the
manufacturer's wear indicator, clamped at zero. SMART power-on hours can exclude
low-power states. “Written per 24 SMART hours” divides lifetime written bytes by
the drive-reported power-on hours and normalizes to 24 hours. It is not necessarily
a calendar-day average and is not the current write speed. A measured calendar-day
average would require counter differences between timestamped observations.
The calendar lifespan forecast and arbitrary HDD health percentage were removed
because they suggested more certainty than SMART provides. See the [NVMe SMART log field definitions](https://manpages.debian.org/testing/libnvme-dev/nvme_smart_log.2.en.html).

## Quick Start
A good SMART result cannot rule out a sudden failure. Keep backups regardless of
the displayed status. SMARTastic makes no network requests; reports are saved
only to the destination you choose.

```bash
## Build from source

Use full **Xcode 26.3 or later** and its command-line tools. If the selected
Command Line Tools SDK lacks the SwiftUI macro plugin, select full Xcode or set
`DEVELOPER_DIR` for the build; see [release documentation](docs/RELEASING.md).

```sh
brew install smartmontools
git clone https://github.com/RobinBially/SMARTastic.git
cd SMARTastic
swift build
bash scripts/make-app.sh
open SMARTastic.app
swift test
./scripts/make-app.sh
open .build/app/SMARTastic.app
```

Or download from [Releases](https://github.com/RobinBially/SMARTastic/releases).
The script creates a release build for the current architecture and signs it ad
hoc for local development. To build both architectures:

## How it works
```sh
ARCHS="arm64 x86_64" ./scripts/make-app.sh
```

| Interface | Data shown |
|-----------|-----------|
| **Sidebar** | All drives with health status, temperature, usage |
| **Detail** | Health overview, usage metrics, life prognosis, drive info |
Launch a synthetic demo for screenshots (no drive reads):

```sh
open .build/app/SMARTastic.app --args --demo --light -AppleLanguages '(en)'
# Use the Appearance switch for System, Light or Dark.
```

HDDs behind USB bridges that don't pass SMART commands show basic info (model, size) with a note that SMART is unavailable.
Build, signing, notarization, release resumption and Homebrew maintenance are
covered in [docs/RELEASING.md](docs/RELEASING.md). The review findings and actual
verification scope are recorded in [docs/REVIEW-1.1.0.md](docs/REVIEW-1.1.0.md).

## License

MIT
[MIT](LICENSE).
Loading