Skip to content
Open
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
21 changes: 15 additions & 6 deletions .github/workflows/plugin-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,20 @@ jobs:
print(next(iter(versions)))
PY
)"
if [[ "$PLUGIN_KIT_VERSION" == "2" ]]; then
PLUGIN_CATALOG_RELATIVE_PATH="docs/plugins/catalog.json"
PLUGIN_CATALOG_MINIMUM_HOST_VERSION="1.1.6"
if (( PLUGIN_KIT_VERSION < 5 )); then
echo "PluginKit versions below 5 use immutable legacy catalogs and cannot be released by the schema-3 workflow." >&2
exit 1
elif [[ "$PLUGIN_KIT_VERSION" == "5" ]]; then
PLUGIN_CATALOG_RELATIVE_PATH="docs/plugins/v5/catalog.json"
PLUGIN_CATALOG_MINIMUM_HOST_VERSION="1.2.0"
PLUGIN_CATALOG_RELATIVE_PATH="docs/plugins/v5/schema3/catalog.json"
PLUGIN_CATALOG_MINIMUM_HOST_VERSION="1.2.1"
else
PLUGIN_CATALOG_RELATIVE_PATH="docs/plugins/v${PLUGIN_KIT_VERSION}/catalog.json"
PLUGIN_CATALOG_MINIMUM_HOST_VERSION="1.1.6"
PLUGIN_CATALOG_MINIMUM_HOST_VERSION="1.2.1"
fi
{
echo "TAG=$TAG"
echo "PLUGIN_RELEASE_MODE=$MODE"
echo "PLUGIN_RELEASE_REQUIRE_VERSION_BUMP=false"
echo "PLUGIN_RELEASE_SELECTION=$PLUGIN_SELECTION"
echo "PLUGIN_RELEASE_TITLE=MacTools Plugins ${RELEASE_SUFFIX}"
echo "PLUGIN_RELEASE_NOTES_URL=https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
Expand All @@ -119,6 +120,11 @@ jobs:
git fetch --force --tags origin +refs/heads/main:refs/remotes/origin/main
if git show "origin/main:${PLUGIN_CATALOG_RELATIVE_PATH}" > "$PLUGIN_PREVIOUS_CATALOG_PATH" 2>/dev/null; then
echo "Using ${PLUGIN_CATALOG_RELATIVE_PATH} from origin/main as the previous production catalog."
elif [[ "$PLUGIN_KIT_VERSION" == "5" ]] && \
git show origin/main:docs/plugins/v5/catalog.json > "$PLUGIN_PREVIOUS_CATALOG_PATH" 2>/dev/null; then
echo "Using docs/plugins/v5/catalog.json from origin/main as the same-ABI schema migration baseline."
echo "PLUGIN_RELEASE_MODE=all" >> "$GITHUB_ENV"
echo "PLUGIN_RELEASE_REQUIRE_VERSION_BUMP=true" >> "$GITHUB_ENV"
elif PREVIOUS_VERSIONED_CATALOG="$(git ls-tree -r --name-only origin/main docs/plugins | \
python3 -c 'import re,sys; current=int(sys.argv[1]); paths=[path.strip() for path in sys.stdin if re.fullmatch(r"docs/plugins/v\d+/catalog\.json", path.strip())]; candidates=[(int(re.search(r"/v(\d+)/", path).group(1)), path) for path in paths if int(re.search(r"/v(\d+)/", path).group(1)) < current]; print(max(candidates)[1] if candidates else "")' \
"$PLUGIN_KIT_VERSION")" && \
Expand Down Expand Up @@ -147,6 +153,9 @@ jobs:
if [[ -n "$PLUGIN_RELEASE_SELECTION" ]]; then
plan_args+=(--plugins "$PLUGIN_RELEASE_SELECTION")
fi
if [[ "$PLUGIN_RELEASE_REQUIRE_VERSION_BUMP" == "true" ]]; then
plan_args+=(--require-version-bump)
fi

scripts/plugins/plan-plugin-release.py "${plan_args[@]}"

Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ Thanks for your interest in MacTools. Please keep each contribution small and cl
- Custom plugin settings views must reuse `MacToolsPluginKit.PluginSettingsTheme` and `.pluginSettingsCardBackground(.standard/.recessed)`. Do not copy private plugin settings styles, and do not make plugins depend on `Sources/App/SettingsStyle.swift`.
- Call `onStateChange?()` after plugin state changes. Long-running scans, file system work, and system calls should not block the main thread for extended periods.
- User-facing copy is primarily Chinese. Keep it concise, clear, and close to native macOS wording.
- Localize user-facing copy with `.xcstrings`. App/Core copy belongs under `Sources/Resources/Localization`, PluginKit copy under `Sources/MacToolsPluginKit/Resources`, and plugin copy under `Plugins/<PluginName>/Resources`. Plugin `plugin.json` files should keep `displayName`/`summary` as fallbacks and add `localizedMetadata` for marketplace and unloaded-plugin presentation.
- Localize user-facing copy with `.xcstrings`. App/Core copy belongs under `Sources/Resources/Localization`, PluginKit copy under `Sources/MacToolsPluginKit/Resources`, and plugin copy under `Plugins/<PluginName>/Resources`. Plugin `plugin.json` files should keep `displayName`/`summary` as fallbacks and add `localizedMetadata` for marketplace and unloaded-plugin presentation. Pre-install product, capability, privacy, setup, and relationship metadata belongs in the same `plugin.json`; follow `docs/plugins/plugin-manifest.schema.json`. Declare localized product copy once under the source-only `productStrings` table, using `@displayName`, `@summary`, `@localizable.<key>`, `@standardAction.<key>`, `@standardSetup.requirements.<key>`, or all 11 locale values, and make every localized product field reference `@productStrings.<key>`. Keep referenced screenshots under `MarketplaceAssets/`, and never add a parallel marketplace manifest or machine-local dynamic action entries.
- Keep current `plugin.json` runtime envelopes complete. Generated package manifests must contain expanded localization values and match their source metadata; do not edit package copies independently. Legacy manifests must still include runtime-decodable `capabilities` and `permissions`; omitting newer product fields is supported only for PluginKit versions below 5 through the explicit local-debug compatibility flag and must never be used for release catalog generation.
- New plugins should provide localization whenever practical, at minimum for panel copy, settings copy, permission text, and plugin metadata.
- Prefer Apple native frameworks. When adding system frameworks, private include paths, or helper executables inside a plugin bundle, declare the smallest necessary differences in the plugin's own `project.yml`. Bundle resource executables that need separate signing should be listed in `plugin.json.package.signPaths`.
- Plugins that use private Apple frameworks must load them dynamically at runtime and validate the required classes and selectors. Do not statically link private frameworks, and surface unsupported-system errors instead of crashing.
Expand All @@ -68,6 +69,7 @@ Thanks for your interest in MacTools. Please keep each contribution small and cl
- User-visible behavior changes are reflected in `README.md` or the relevant design documentation.
- User-visible app or plugin changes include a concise English changelog fragment in `changes/unreleased/*.md`.
- Plugin manifest `capabilities.settings` (`none`, `form`, or `workspace`) matches the runtime `settingsPage` layout.
- Rich manifest static and dynamic action descriptors match the runtime provider/action identity, risk, permissions, external policy, automation eligibility, and parameter portability.
- High-risk features cover safety checks, error states, and missing-permission cases.
- The PR does not include unrelated formatting, generated files, local configuration, certificates, or release credentials.

Expand All @@ -84,5 +86,5 @@ Thanks for your interest in MacTools. Please keep each contribution small and cl
- If Apple notarization is needed, store credentials first with `xcrun notarytool store-credentials`.
- Version numbers default to `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` in `Configs/AppVersion.xcconfig`.
- Local production builds can still use the lower-level script: `./scripts/release-local.sh`; before publishing to GitHub Releases, run `gh auth login`, then `./scripts/release-local.sh --publish`.
- Plugin library releases are triggered by `plugins-*` batch tags through the `Plugin Release` workflow. Within one PluginKit ABI line, plugins with bumped versions are built and uploaded, then merged into that line's catalog. Changes under `Sources/MacToolsPluginKit/` require rebuilding and bumping every plugin so the catalog cannot retain binaries linked against an older shared framework. The standard `make release` flow performs these manifest bumps in the release commit; feature PRs should not pre-bump unrelated plugins. The first release of a new ABI also rebuilds every plugin and writes a versioned catalog. MacTools through 1.1.6 keeps reading the immutable PluginKit v4 catalog at `docs/plugins/v4/catalog.json`; MacTools 1.2 and later use PluginKit v5 at `docs/plugins/v5/catalog.json`. Publish the v5 plugin batch and catalog first, wait for Pages to serve the committed signed catalog, and only then prepare or publish the 1.2 app. The app release helper and final release workflow fail closed unless that deployed catalog exactly matches the committed catalog and has a valid signature. The catalog private key, Developer ID certificate, and GitHub token must come from CI secrets or local environment variables.
- Plugin library releases are triggered by `plugins-*` batch tags through the `Plugin Release` workflow. Within one PluginKit ABI and catalog-schema compatibility line, plugins with bumped versions are built and uploaded, then merged into that line's catalog. Changes under `Sources/MacToolsPluginKit/` require rebuilding and bumping every plugin so the catalog cannot retain binaries linked against an older shared framework. The standard `make release` flow performs these manifest bumps in the release commit; feature PRs should not pre-bump unrelated plugins. The first release of a new ABI or schema line also rebuilds every plugin and writes a separate catalog. MacTools through 1.1.6 keeps reading the immutable PluginKit v4 catalog at `docs/plugins/v4/catalog.json`; MacTools 1.2.0 keeps reading PluginKit v5 schema 2 at `docs/plugins/v5/catalog.json`; schema-3 hosts read `docs/plugins/v5/schema3/catalog.json`. Publish the compatible plugin batch and catalog first, wait for Pages to serve the committed signed catalog, and only then prepare or publish the corresponding app. The app release helper and final release workflow fail closed unless that deployed catalog exactly matches the committed catalog and has a valid signature. The catalog private key, Developer ID certificate, and GitHub token must come from CI secrets or local environment variables.
- GitHub Actions build and release configuration is documented in `docs/github-actions.md`; plugin catalog, package structure, and batch release flows are documented in `docs/plugins/plugin-catalog.md`.
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ PLUGIN_RELEASE_DIST_DIR ?= build/PluginRelease
PLUGIN_RELEASE_ASSETS_DIR ?= $(PLUGIN_RELEASE_DIST_DIR)/Assets
PLUGIN_RELEASE_CATALOG ?= $(PLUGIN_RELEASE_DIST_DIR)/catalog.json
PLUGIN_KIT_VERSION ?= $(shell $(PYTHON3) -c 'import glob,json; versions={json.load(open(path, encoding="utf-8"))["pluginKitVersion"] for path in glob.glob("Plugins/*/plugin.json")}; print(next(iter(versions)) if len(versions) == 1 else "")')
PLUGIN_RELEASE_SIGNED_CATALOG ?= $(if $(filter 2,$(PLUGIN_KIT_VERSION)),docs/plugins/catalog.json,docs/plugins/v$(PLUGIN_KIT_VERSION)/catalog.json)
PLUGIN_CATALOG_MINIMUM_HOST_VERSION ?= $(if $(filter 5,$(PLUGIN_KIT_VERSION)),1.2.0,1.1.6)
PLUGIN_RELEASE_SIGNED_CATALOG ?= $(if $(filter 5,$(PLUGIN_KIT_VERSION)),docs/plugins/v5/schema3/catalog.json,$(if $(filter 2,$(PLUGIN_KIT_VERSION)),docs/plugins/catalog.json,docs/plugins/v$(PLUGIN_KIT_VERSION)/catalog.json))
PLUGIN_CATALOG_MINIMUM_HOST_VERSION ?= 1.2.1
PLUGIN_RELEASE_BASE_URL ?= https://github.com/$(PLUGIN_RELEASE_REPO)/releases/download/$(PLUGIN_RELEASE_TAG)
E2E_SCRIPT := scripts/e2e/mactools-e2e.sh
E2E_SESSION ?=
Expand Down Expand Up @@ -128,6 +128,12 @@ generate-icon-gallery:
--output-dir "$(LOCAL_ICON_GALLERY_DIR)"

package-plugins-release: generate
@case " 1 2 3 4 " in \
*" $(PLUGIN_KIT_VERSION) "*) \
echo "PluginKit versions below 5 use immutable legacy catalogs and cannot be released by the schema-3 tooling." >&2; \
exit 1; \
;; \
esac
@./scripts/plugins/build-plugin-release-assets.sh \
--source-dir "$(LOCAL_PLUGIN_SOURCE_DIR)" \
--build-dir "$(PLUGIN_RELEASE_BUILD_DIR)" \
Expand Down
152 changes: 151 additions & 1 deletion Plugins/ActionGrid/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
"summary": "在指標附近開啟使用者設定的常用操作網格。"
}
},
"productStrings": {
"display-name": "@displayName",
"summary": "@summary"
},
"version": "1.0.0",
"minHostVersion": "1.2.0",
"pluginKitVersion": 5,
Expand All @@ -63,5 +67,151 @@
"settings": "workspace"
},
"permissions": [],
"category": "system"
"category": "system",
"presentation": {
"longDescription": "@productStrings.summary",
"examples": [
{
"id": "primary-use",
"text": "@productStrings.summary"
}
],
"screenshots": [],
"documentationURL": "https://github.com/ggbond268/MacTools#plugins-and-settings",
"supportURL": "https://github.com/ggbond268/MacTools/issues",
"publisher": "MacTools",
"license": "Apache-2.0"
},
"discovery": {
"keywords": [
"action",
"grid",
"system"
],
"localizedSynonyms": {
"ar": [
"شبكة الإجراءات"
],
"de": [
"Aktionsraster"
],
"en": [
"Action Grid"
],
"es": [
"Cuadrícula de acciones"
],
"fr": [
"Grille d’actions"
],
"ja": [
"アクショングリッド"
],
"ko": [
"동작 그리드"
],
"pt": [
"Grade de ações"
],
"ru": [
"Сетка действий"
],
"zh-Hans": [
"操作网格"
],
"zh-Hant": [
"操作網格"
]
},
"useCases": [
{
"id": "primary-use",
"title": "@productStrings.display-name"
}
],
"goalCategories": [
"system"
],
"relatedPluginIDs": [],
"alternativePluginIDs": []
},
"requirements": {
"minimumMacOSVersion": "14.0",
"architectures": [
"arm64",
"x86_64"
],
"hardware": [],
"applications": [],
"executables": [],
"permissionIDs": [],
"setupComplexity": "none",
"requiresRelaunch": false
},
"privacy": {
"dataObserved": [
"system-state"
],
"dataPersisted": [
"plugin-configuration"
],
"retention": {
"policy": "user-controlled"
},
"networkUse": "none",
"networkDomains": [],
"allowsUserConfiguredDomains": false,
"telemetry": "none",
"processesSensitiveUserContent": false,
"diagnosticExportsContainUserData": false
},
"setup": {
"steps": [],
"optionalSurfaces": []
},
"relationships": {
"relatedPluginIDs": [],
"includedPackIDs": [],
"suggestedRecipeIDs": [],
"supersedesPluginIDs": []
},
"actions": {
"providers": [
{
"id": "action-grid",
"kind": "static",
"staticActions": [
{
"id": "show",
"title": "@productStrings.display-name",
"description": "@productStrings.summary",
"keywords": [
"操作网格",
"显示操作网格",
"action",
"grid",
"launcher",
"show"
],
"systemImage": "square.grid.3x3",
"parameters": [],
"permissionIDs": [],
"risk": "safe",
"surfaces": [
"unified-search",
"global-shortcut",
"run-link",
"workflow",
"action-grid",
"trackpad-gesture",
"manual"
],
"automaticEligible": false,
"externalInvocation": "allowed"
}
],
"dynamicTemplates": []
}
]
}
}
41 changes: 38 additions & 3 deletions Plugins/ActivityBar/Sources/ActivityBarPlugin.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import CoreGraphics
import SwiftUI
import MacToolsPluginKit

Expand All @@ -23,11 +24,16 @@ private struct ActivityBarPluginProvider: PluginProvider {
}

@MainActor
final class ActivityBarPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginComponentPanel, PluginActionProviding {
final class ActivityBarPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginComponentPanel,
PluginActionProviding, PluginActionPermissionProviding
{
private enum ActionID {
static let setTrackingEnabled = "set-tracking-enabled"
static let resetToday = "reset-today"
}
private enum PermissionID {
static let inputMonitoring = "inputMonitoring"
}

private struct SettingsStatus {
let text: String
Expand Down Expand Up @@ -112,6 +118,20 @@ final class ActivityBarPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginCompone
)
}

var permissionRequirements: [PluginPermissionRequirement] {
[
PluginPermissionRequirement(
id: PermissionID.inputMonitoring,
kind: .inputMonitoring,
title: localization.string("settings.inputMonitoring.title", defaultValue: "输入监控"),
description: localization.string(
"settings.inputMonitoring.description",
defaultValue: "用于统计键盘、鼠标点击和滚动事件。"
)
),
]
}

var settingsPage: PluginSettingsPage? {
.form(
description: metadata.defaultDescription,
Expand Down Expand Up @@ -274,10 +294,25 @@ final class ActivityBarPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginCompone
}

func permissionState(for permissionID: String) -> PluginPermissionState {
PluginPermissionState(isGranted: true, footnote: nil)
guard permissionID == PermissionID.inputMonitoring else {
return PluginPermissionState(isGranted: true, footnote: nil)
}
return PluginPermissionState(
isGranted: CGPreflightListenEventAccess(),
footnote: controller.inputMonitoringFootnote
)
}

func handlePermissionAction(id: String) {}
func handlePermissionAction(id: String) {
guard id == PermissionID.inputMonitoring else { return }
controller.openInputMonitoringSettings()
}

func permissionRequirementIDs(for actionKey: ActionKey) -> [String] {
guard actionKey.providerID == metadata.id,
actionKey.actionID == ActionID.setTrackingEnabled else { return [] }
return [PermissionID.inputMonitoring]
}

func handleSettingsAction(_ action: PluginSettingsAction) {
guard case let .invoke(controlID) = action else { return }
Expand Down
Loading
Loading