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
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: CI

on:
push:
branches:
- main
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Install SwiftLint
run: brew install swiftlint
- name: Run SwiftLint
run: swiftlint --strict

format:
name: Format
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Install SwiftFormat
run: brew install swiftformat
# Paths come first: --lint would otherwise swallow the one after it.
- name: Check formatting
run: swiftformat Sources Preview --lint

build:
name: Build and Analyse
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Install SwiftLint
run: brew install swiftlint
- name: Build and Analyse
run: |
set -o pipefail
for scheme in Macstify MacstifyPreview; do
xcodebuild build analyze \
-project Macstify.xcodeproj \
-scheme "$scheme" \
-destination 'generic/platform=macOS' \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
| tee -a build.log
done
- name: SwiftLint Analyse
run: swiftlint analyze --strict --compiler-log-path build.log
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6.0
1 change: 1 addition & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--swiftversion 6.0
16 changes: 16 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
excluded:
- build # xcodebuild output

# Require trailing commas to match SwiftFormat's trailingCommas rule.
trailing_comma:
mandatory_comma: true

# Geometry code uses short names like x, y, dx, dy.
identifier_name:
min_length:
warning: 1
error: 1

analyzer_rules:
- unused_declaration
- unused_import
78 changes: 49 additions & 29 deletions Preview/PreviewApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import AppKit
enum PreviewApp {
private static var delegate: PreviewDelegate?

// Called by the Swift runtime through @main, never from code.
// swiftlint:disable:next unused_declaration
static func main() {
let options: Options
do {
Expand Down Expand Up @@ -112,13 +114,17 @@ enum PreviewApp {
_ = NSApplication.shared

var settings = MacstifySettings.load()
if isPreview { settings = settings.previewAdjusted }
if let speed { settings.speed = speed }
if isPreview {
settings = settings.previewAdjusted
}
if let speed {
settings.speed = speed
}

let rect = NSRect(origin: .zero, size: size)
let engine = MacstifyEngine(settings: settings)
engine.reset(bounds: rect)
for _ in 0..<frames {
for _ in 0 ..< frames {
engine.step(dt: 1.0 / 60.0)
}

Expand Down Expand Up @@ -187,7 +193,9 @@ enum PreviewApp {
}

private final class PreviewDelegate: NSObject, NSApplicationDelegate {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
true
}
}

private struct Options {
Expand All @@ -203,34 +211,13 @@ private struct Options {
var iterator = arguments.makeIterator()
while let argument = iterator.next() {
switch argument {
case "--snapshot":
snapshotPath = try Self.value(after: argument, from: &iterator)
case "--frames":
let raw = try Self.value(after: argument, from: &iterator)
guard let value = Int(raw), value > 0 else {
throw OptionError("--frames expects a positive integer, got \(raw)")
}
frames = value
case "--size":
let raw = try Self.value(after: argument, from: &iterator)
let parts = raw.lowercased().split(separator: "x")
guard parts.count == 2,
let width = Double(parts[0]), let height = Double(parts[1]),
width > 0, height > 0 else {
throw OptionError("--size expects WIDTHxHEIGHT, got \(raw)")
}
size = NSSize(width: width, height: height)
explicitSize = true
case "--speed":
let raw = try Self.value(after: argument, from: &iterator)
guard let value = Double(raw), value > 0 else {
throw OptionError("--speed expects a positive number, got \(raw)")
}
speed = value
case "--options":
showOptions = true
case "--preview":
isPreview = true
case "--snapshot", "--frames", "--size", "--speed":
let raw = try Self.value(after: argument, from: &iterator)
try set(argument, to: raw, explicitSize: &explicitSize)
default:
throw OptionError("unknown argument: \(argument)")
}
Expand All @@ -242,6 +229,37 @@ private struct Options {
}
}

private mutating func set(_ flag: String, to raw: String, explicitSize: inout Bool) throws {
switch flag {
case "--snapshot":
snapshotPath = raw
case "--frames":
guard let value = Int(raw), value > 0 else {
throw OptionError("\(flag) expects a positive integer, got \(raw)")
}
frames = value
case "--size":
size = try Self.parseSize(raw)
explicitSize = true
default:
guard let value = Double(raw), value > 0 else {
throw OptionError("\(flag) expects a positive number, got \(raw)")
}
speed = value
}
}

private static func parseSize(_ raw: String) throws -> NSSize {
let parts = raw.lowercased().split(separator: "x")
guard parts.count == 2,
let width = Double(parts[0]), let height = Double(parts[1]),
width > 0, height > 0
else {
throw OptionError("--size expects WIDTHxHEIGHT, got \(raw)")
}
return NSSize(width: width, height: height)
}

private static func value(
after flag: String,
from iterator: inout some IteratorProtocol<String>
Expand All @@ -255,5 +273,7 @@ private struct Options {

private struct OptionError: LocalizedError {
let errorDescription: String?
init(_ message: String) { errorDescription = message }
init(_ message: String) {
errorDescription = message
}
}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ snapshots are written at 2x, as on a Retina display.

The screenshots above were produced with `--snapshot`.

CI builds and analyses both schemes and runs the same lint and format checks you can run locally:

```sh
swiftlint --strict
swiftformat Sources Preview --lint
```

### Thumbnail

The picker in System Settings shows `Contents/Resources/thumbnail.png` and `thumbnail@2x.png`
Expand Down
126 changes: 57 additions & 69 deletions Sources/ConfigureSheetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,60 +39,65 @@ final class ConfigureSheetController: NSObject {
// MARK: - Interface

private func buildInterface() {
let integer: (Double) -> String = { String(Int($0.rounded())) }
let multiplier: (Double) -> String = { String(format: "%.2f×", $0) }
let root = NSStackView(views: [makeGrid(), makeButtons()])
root.orientation = .vertical
root.alignment = .width
root.spacing = 20
root.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
root.translatesAutoresizingMaskIntoConstraints = false

let gridRows: [[NSView]] = [
makeRow(
title: "Shapes:",
control: makeStepper(range: MacstifySettings.polygonCountRange),
describe: integer,
read: { Double($0.polygonCount) },
write: { $0.polygonCount = Int($1.rounded()) }
),
makeRow(
title: "Vertices:",
control: makeStepper(range: MacstifySettings.vertexCountRange),
describe: integer,
read: { Double($0.vertexCount) },
write: { $0.vertexCount = Int($1.rounded()) }
),
makeRow(
title: "Trail length:",
control: makeSlider(range: MacstifySettings.trailLengthRange),
describe: integer,
read: { Double($0.trailLength) },
write: { $0.trailLength = Int($1.rounded()) }
),
makeRow(
title: "Speed:",
control: makeSlider(range: MacstifySettings.speedRange),
describe: multiplier,
read: { $0.speed },
write: { $0.speed = $1 }
),
makeRow(
title: "Line width:",
control: makeSlider(range: MacstifySettings.lineWidthRange),
describe: { String(format: "%.1f pt", $0) },
read: { $0.lineWidth },
write: { $0.lineWidth = $1 }
),
makeRow(
title: "Color speed:",
control: makeSlider(range: MacstifySettings.colorSpeedRange),
describe: multiplier,
read: { $0.colorSpeed },
write: { $0.colorSpeed = $1 }
),
]
let content = NSView()
content.addSubview(root)
NSLayoutConstraint.activate([
root.leadingAnchor.constraint(equalTo: content.leadingAnchor),
root.trailingAnchor.constraint(equalTo: content.trailingAnchor),
root.topAnchor.constraint(equalTo: content.topAnchor),
root.bottomAnchor.constraint(equalTo: content.bottomAnchor),
])

let grid = NSGridView(views: gridRows)
window.contentView = content
content.layoutSubtreeIfNeeded()
window.setContentSize(content.fittingSize)
}

private func makeGrid() -> NSGridView {
let grid = NSGridView(views: makeRows())
grid.translatesAutoresizingMaskIntoConstraints = false
grid.column(at: 0).xPlacement = .trailing
grid.rowSpacing = 10
grid.columnSpacing = 10
return grid
}

private func makeRows() -> [[NSView]] {
let integer: (Double) -> String = { String(Int($0.rounded())) }
let multiplier: (Double) -> String = { String(format: "%.2f×", $0) }
let points: (Double) -> String = { String(format: "%.1f pt", $0) }
let settings = MacstifySettings.self

return [
makeRow(title: "Shapes:", control: makeStepper(range: settings.polygonCountRange),
describe: integer, read: { Double($0.polygonCount) },
write: { $0.polygonCount = Int($1.rounded()) }),
makeRow(title: "Vertices:", control: makeStepper(range: settings.vertexCountRange),
describe: integer, read: { Double($0.vertexCount) },
write: { $0.vertexCount = Int($1.rounded()) }),
makeRow(title: "Trail length:", control: makeSlider(range: settings.trailLengthRange),
describe: integer, read: { Double($0.trailLength) },
write: { $0.trailLength = Int($1.rounded()) }),
makeRow(title: "Speed:", control: makeSlider(range: settings.speedRange),
describe: multiplier, read: { $0.speed },
write: { $0.speed = $1 }),
makeRow(title: "Line width:", control: makeSlider(range: settings.lineWidthRange),
describe: points, read: { $0.lineWidth },
write: { $0.lineWidth = $1 }),
makeRow(title: "Color speed:", control: makeSlider(range: settings.colorSpeedRange),
describe: multiplier, read: { $0.colorSpeed },
write: { $0.colorSpeed = $1 }),
]
}

private func makeButtons() -> NSStackView {
let restore = NSButton(title: "Restore Defaults", target: self,
action: #selector(restoreDefaults))
let cancel = NSButton(title: "Cancel", target: self, action: #selector(cancel))
Expand All @@ -105,26 +110,7 @@ final class ConfigureSheetController: NSObject {
buttons.spacing = 12
buttons.setViews([restore], in: .leading)
buttons.setViews([cancel, ok], in: .trailing)

let root = NSStackView(views: [grid, buttons])
root.orientation = .vertical
root.alignment = .width
root.spacing = 20
root.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
root.translatesAutoresizingMaskIntoConstraints = false

let content = NSView()
content.addSubview(root)
NSLayoutConstraint.activate([
root.leadingAnchor.constraint(equalTo: content.leadingAnchor),
root.trailingAnchor.constraint(equalTo: content.trailingAnchor),
root.topAnchor.constraint(equalTo: content.topAnchor),
root.bottomAnchor.constraint(equalTo: content.bottomAnchor),
])

window.contentView = content
content.layoutSubtreeIfNeeded()
window.setContentSize(content.fittingSize)
return buttons
}

private func makeRow(
Expand Down Expand Up @@ -172,7 +158,7 @@ final class ConfigureSheetController: NSObject {
}

private func makeSlider(range: ClosedRange<Int>) -> NSSlider {
makeSlider(range: Double(range.lowerBound)...Double(range.upperBound))
makeSlider(range: Double(range.lowerBound) ... Double(range.upperBound))
}

/// Pushes `settings` back out to every control, so one refresh path serves
Expand Down Expand Up @@ -212,7 +198,9 @@ final class ConfigureSheetController: NSObject {
parent.endSheet(window, returnCode: saved ? .OK : .cancel)
} else {
// Reachable from the preview app, which shows the sheet standalone.
if NSApp.modalWindow === window { NSApp.stopModal() }
if NSApp.modalWindow === window {
NSApp.stopModal()
}
window.orderOut(nil)
}
onDismiss(saved)
Expand Down
Loading
Loading