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
10 changes: 10 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

if ! command -v just >/dev/null 2>&1; then
echo "The TypeSwitch pre-commit hook requires just." >&2
echo "Install it manually with: brew install just" >&2
exit 1
fi

exec just format-staged
63 changes: 63 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: PR Checks

on:
pull_request:

permissions:
contents: read

concurrency:
group: pr-checks-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
format:
name: Format
runs-on: macos-26

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install just
run: brew install just

- name: Install pinned SwiftFormat
run: |
SWIFTFORMAT_VERSION="$(tr -d '[:space:]' < .swiftformat-version)"
SWIFTFORMAT_DIR="$RUNNER_TEMP/swiftformat"
mkdir -p "$SWIFTFORMAT_DIR"
curl -fsSL \
"https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat.zip" \
-o "$RUNNER_TEMP/swiftformat.zip"
EXPECTED_SHA256="$(tr -d '[:space:]' < .swiftformat-checksum)"
ACTUAL_SHA256="$(shasum -a 256 "$RUNNER_TEMP/swiftformat.zip" | awk '{print $1}')"
if [[ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]]; then
echo "::error title=SwiftFormat checksum mismatch::Expected ${EXPECTED_SHA256}, got ${ACTUAL_SHA256}."
exit 1
fi
unzip -q "$RUNNER_TEMP/swiftformat.zip" -d "$SWIFTFORMAT_DIR"
chmod +x "$SWIFTFORMAT_DIR/swiftformat"
echo "$SWIFTFORMAT_DIR" >> "$GITHUB_PATH"

- name: Check formatting
run: just lint-ci

tests:
name: Tests
runs-on: macos-26

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.2'

- name: Install tools
run: brew install just tuist

- name: Run tests
run: just test
8 changes: 8 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--min-version 0.62.1
--swift-version 5.9
--language-mode 5
--indent 4
--ifdef no-indent
--linebreaks lf
--exclude .build,Derived,Tuist/.build
--rules blankLineAfterImports,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenImports,blankLinesBetweenScopes,braces,consecutiveBlankLines,consecutiveSpaces,duplicateImports,elseOnSameLine,emptyBraces,indent,linebreakAtEndOfFile,linebreaks,sortImports,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBraces,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,trailingCommas,trailingSpace,wrapArguments,wrapMultilineStatementBraces
1 change: 1 addition & 0 deletions .swiftformat-checksum
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7cb1cb1fae04932047c7015441c543848e8e60e1572d808d080e0a1f1661114a
1 change: 1 addition & 0 deletions .swiftformat-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.62.1
4 changes: 2 additions & 2 deletions Design/AppIcon/Sources/render_typeswitch_icon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ private func writePNG(context: CGContext, output: URL) throws {
}
CGImageDestinationAddImage(destination, image, [
kCGImagePropertyPNGDictionary: [
kCGImagePropertyPNGInterlaceType: 0
]
kCGImagePropertyPNGInterlaceType: 0,
],
] as CFDictionary)
if !CGImageDestinationFinalize(destination) {
fatalError("Unable to write \(output.path).")
Expand Down
35 changes: 18 additions & 17 deletions Project.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ProjectDescription

// MARK: - Version

let appVersion = Environment.appVersion.getString(default: "0.0.0")
let buildVersion = Environment.buildVersion.getString(default: "0")

Expand All @@ -17,7 +18,7 @@ let project = Project(
.remote(url: "https://github.com/pointfreeco/swift-perception", requirement: .upToNextMajor(from: "2.0.10")),
.remote(url: "https://github.com/pointfreeco/swift-sharing", requirement: .upToNextMajor(from: "2.9.1")),
.remote(url: "https://github.com/sparkle-project/Sparkle", requirement: .upToNextMajor(from: "2.9.4")),
.remote(url: "https://github.com/SwifterSwift/SwifterSwift", requirement: .upToNextMajor(from: "8.0.0"))
.remote(url: "https://github.com/SwifterSwift/SwifterSwift", requirement: .upToNextMajor(from: "8.0.0")),
],
settings: .settings(
base: [
Expand All @@ -29,11 +30,11 @@ let project = Project(
// 宏定义支持
"SWIFT_STRICT_CONCURRENCY": SettingValue(stringLiteral: "complete"),
"ENABLE_MACROS": SettingValue(stringLiteral: "YES"),
"SWIFT_MACRO_DEBUGGING": SettingValue(stringLiteral: "YES")
"SWIFT_MACRO_DEBUGGING": SettingValue(stringLiteral: "YES"),
],
configurations: [
.debug(name: "Debug"),
.release(name: "Release")
.release(name: "Release"),
]
),
targets: [
Expand All @@ -44,24 +45,24 @@ let project = Project(
bundleId: "top.ygsgdbd.TypeSwitch",
deploymentTargets: .macOS("14.0"),
infoPlist: .dictionary([
"LSUIElement": true, // 设置为纯菜单栏应用
"LSUIElement": true, // 设置为纯菜单栏应用
"CFBundleExecutable": "$(EXECUTABLE_NAME)",
"CFBundleDevelopmentRegion": "zh-Hans", // 设置默认开发区域为简体中文
"CFBundleDevelopmentRegion": "zh-Hans", // 设置默认开发区域为简体中文
"CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleLocalizations": ["zh-Hans", "zh-Hant", "en"], // 支持的语言列表
"CFBundleLocalizations": ["zh-Hans", "zh-Hant", "en"], // 支持的语言列表
"CFBundleName": "$(PRODUCT_NAME)",
"CFBundlePackageType": "APPL",
"AppleLanguages": ["zh-Hans"], // 设置默认语言为简体中文
"AppleLanguages": ["zh-Hans"], // 设置默认语言为简体中文
"NSHumanReadableCopyright": "Copyright © 2024 ygsgdbd. All rights reserved.",
"LSApplicationCategoryType": "public.app-category.utilities",
"LSMinimumSystemVersion": "14.0",
"NSPrincipalClass": "NSApplication",
"CFBundleShortVersionString": .string(appVersion), // 市场版本号
"CFBundleVersion": .string(buildVersion), // 构建版本号
"CFBundleShortVersionString": .string(appVersion), // 市场版本号
"CFBundleVersion": .string(buildVersion), // 构建版本号
"SUFeedURL": "https://github.com/ygsgdbd/TypeSwitch/releases/latest/download/appcast.xml",
"SUPublicEDKey": "tzapCS0NiXBf0Alm3T2veTCaauxr2dmMCnIFYaK+lOs=",
"SUEnableAutomaticChecks": false
"SUEnableAutomaticChecks": false,
]),
sources: ["TypeSwitch/Sources/**"],
resources: ["TypeSwitch/Resources/**"],
Expand All @@ -73,7 +74,7 @@ let project = Project(
.package(product: "PerceptionCore"),
.package(product: "Sharing"),
.package(product: "Sparkle"),
.package(product: "SwifterSwift")
.package(product: "SwifterSwift"),
],
settings: .settings(
base: [
Expand All @@ -82,11 +83,11 @@ let project = Project(
"SWIFT_STRICT_CONCURRENCY": "complete",
"ENABLE_MACROS": "YES",
"SWIFT_MACRO_DEBUGGING": "YES",
"SWIFT_MACRO_EXPANSION": "YES"
"SWIFT_MACRO_EXPANSION": "YES",
],
configurations: [
.debug(name: "Debug"),
.release(name: "Release")
.release(name: "Release"),
]
)
),
Expand All @@ -103,20 +104,20 @@ let project = Project(
.target(name: "TypeSwitch"),
.package(product: "ComposableArchitecture"),
.package(product: "Dependencies"),
.package(product: "Sharing")
.package(product: "Sharing"),
],
settings: .settings(
base: [
"BUNDLE_LOADER": "$(TEST_HOST)",
"SWIFT_VERSION": "5.9",
"TEST_HOST": "$(BUILT_PRODUCTS_DIR)/TypeSwitch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TypeSwitch",
"TEST_TARGET_NAME": "TypeSwitch"
"TEST_TARGET_NAME": "TypeSwitch",
],
configurations: [
.debug(name: "Debug"),
.release(name: "Release")
.release(name: "Release"),
]
)
)
),
]
)
3 changes: 2 additions & 1 deletion TypeSwitch/Sources/App/AppFeature+MenuState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ extension AppFeature.State {

var currentAppMenuItem: AppMenuItem? {
guard let currentFrontmostBundleId,
let appInfo = knownAppInfo(for: currentFrontmostBundleId) else {
let appInfo = knownAppInfo(for: currentFrontmostBundleId)
else {
return nil
}

Expand Down
18 changes: 11 additions & 7 deletions TypeSwitch/Sources/App/ReadmeScreenshotConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ struct ReadmeScreenshotConfiguration: Equatable {

if let optionIndex = arguments.firstIndex(of: "--readme-appearance"),
arguments.indices.contains(optionIndex + 1),
let appearance = Appearance(rawValue: arguments[optionIndex + 1]) {
let appearance = Appearance(rawValue: arguments[optionIndex + 1])
{
self.appearance = appearance
} else {
self.appearance = .light
}

if let optionIndex = arguments.firstIndex(of: "--readme-display-id"),
arguments.indices.contains(optionIndex + 1),
let displayID = CGDirectDisplayID(arguments[optionIndex + 1]) {
let displayID = CGDirectDisplayID(arguments[optionIndex + 1])
{
self.requestedDisplayID = displayID
} else {
self.requestedDisplayID = nil
Expand All @@ -73,7 +75,8 @@ struct ReadmeScreenshotConfiguration: Equatable {
?? Self.highestResolutionDisplayID(in: Self.onlineDisplays())
let screen = NSScreen.screens.first { screen in
guard let number = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")]
as? NSNumber else {
as? NSNumber
else {
return false
}
return number.uint32Value == targetDisplayID
Expand Down Expand Up @@ -153,7 +156,7 @@ struct ReadmeScreenshotConfiguration: Equatable {
strategy: .fixed(inputMethodId: "com.example.missing-input-method"),
createdAt: now,
updatedAt: now
)
),
]

return AppFeature.State(
Expand All @@ -162,7 +165,7 @@ struct ReadmeScreenshotConfiguration: Equatable {
value: AppSwitchStatisticsStore(
counts: [
safari.bundleId: 18,
terminal.bundleId: 33
terminal.bundleId: 33,
]
)
),
Expand All @@ -172,7 +175,7 @@ struct ReadmeScreenshotConfiguration: Equatable {
currentFrontmostBundleId: safari.bundleId,
inputMethods: [
InputMethod(id: abcID, name: "ABC"),
InputMethod(id: pinyinID, name: "Pinyin")
InputMethod(id: pinyinID, name: "Pinyin"),
],
isReadmeDemo: true,
launchAtLoginStatus: .enabled,
Expand All @@ -183,7 +186,8 @@ struct ReadmeScreenshotConfiguration: Equatable {
private static func onlineDisplays() -> [Display] {
var displayCount: UInt32 = 0
guard CGGetOnlineDisplayList(0, nil, &displayCount) == .success,
displayCount > 0 else {
displayCount > 0
else {
return []
}

Expand Down
8 changes: 4 additions & 4 deletions TypeSwitch/Sources/App/TypeSwitchApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct TypeSwitchApp: App {
let updaterController: SPUStandardUpdaterController

init() {
#if DEBUG
#if DEBUG
let readmeConfiguration = ReadmeScreenshotConfiguration(
arguments: ProcessInfo.processInfo.arguments
)
Expand All @@ -22,13 +22,13 @@ struct TypeSwitchApp: App {
self.readmeColorScheme = readmeConfiguration?.colorScheme
let initialState = readmeConfiguration?.initialState(now: Date()) ?? AppFeature.State()
let startsLiveServices = readmeConfiguration == nil
#else
#else
let readmeMenuAppearance: NSAppearance? = nil
self.readmeBackdropWindow = nil
self.readmeColorScheme = nil
let initialState = AppFeature.State()
let startsLiveServices = true
#endif
#endif
let store = Store(initialState: initialState) {
AppFeature()
}
Expand All @@ -52,7 +52,7 @@ struct TypeSwitchApp: App {
item.submenu?.appearance = readmeMenuAppearance
}
}
}
},
]
} else {
self.menuTrackingObservers = []
Expand Down
3 changes: 2 additions & 1 deletion TypeSwitch/Sources/Core/Models/AppInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ struct AppInfo: Identifiable, Sendable, Hashable {
init?(bundleURL: URL) {
guard let bundle = Bundle(url: bundleURL),
let bundleId = bundle.bundleIdentifier,
let name = Self.bundleName(from: bundle) else {
let name = Self.bundleName(from: bundle)
else {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import AppKit
import Foundation

/// 应用信息服务类,负责管理应用相关链接
enum AppInfoService {
Expand Down
14 changes: 8 additions & 6 deletions TypeSwitch/Sources/Services/AppManagement/AppListService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import AppKit
import Foundation
import SwifterSwift

/// 应用列表服务类
Expand All @@ -8,9 +8,9 @@ enum AppListService {
/// 应用搜索目录列表
/// 按优先级排序:用户应用目录 > 系统应用目录
static let applicationDirs = [
"/Applications", // 用户安装的应用
"~/Applications", // 用户主目录下的应用
"/System/Applications" // 系统应用
"/Applications", // 用户安装的应用
"~/Applications", // 用户主目录下的应用
"/System/Applications", // 系统应用
].map { NSString(string: $0).expandingTildeInPath }

@MainActor
Expand Down Expand Up @@ -93,7 +93,8 @@ enum AppListService {
let fileManager = FileManager.default

guard fileManager.fileExists(atPath: dir),
fileManager.isReadableFile(atPath: dir) else {
fileManager.isReadableFile(atPath: dir)
else {
print("⚠️ 无法访问目录: \(dir)")
return [:]
}
Expand All @@ -119,7 +120,8 @@ enum AppListService {

if let app = AppInfo(bundleURL: fileURL),
bundleIds.contains(app.bundleId),
app.bundleId != Bundle.main.bundleIdentifier {
app.bundleId != Bundle.main.bundleIdentifier
{
matchedApps[app.bundleId] = app
if matchedApps.count == bundleIds.count {
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import OSLog
/// 负责获取、切换和管理系统输入法
@MainActor
enum InputMethodService {

/// 输入法相关错误
enum InputMethodError: Error, LocalizedError {
case failedToFetchInputMethods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private extension LaunchAtLoginService {
let plist: [String: Any] = [
"Label": launchAgentIdentifier,
"ProgramArguments": [executableURL.path],
"RunAtLoad": true
"RunAtLoad": true,
]
let data = try PropertyListSerialization.data(
fromPropertyList: plist,
Expand Down
Loading
Loading