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
4 changes: 2 additions & 2 deletions Sources/KitchenSyncApp/Design/KSTokens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ enum KS {
/// Text drawn ON lime.
static let onInk = Color(hex: 0x0A0D07)
static let amber = Color(hex: 0xFF9D3B)
/// The stopped label — salmon, not grey.
/// The stopped label — salmon, not gray.
static let ember = Color(hex: 0xFF7A6B)

static let ledFill = LinearGradient(colors: [Color(hex: 0xCAFF5A), Color(hex: 0x9BE32A)],
Expand Down Expand Up @@ -75,7 +75,7 @@ enum KS {
// (the BPM readout). All three are SIL OFL 1.1 and SHOULD be bundled to keep
// the identity 1:1. They are not bundled yet — these are the documented
// fallbacks. Note the design direction's rule: if DSEG7 is absent, the tempo
// glass must DROP its ghost layer, because a grey duplicate of a non-segmented
// glass must DROP its ghost layer, because a gray duplicate of a non-segmented
// font reads as a rendering bug rather than unlit segments.

extension Font {
Expand Down
2 changes: 1 addition & 1 deletion Sources/KitchenSyncApp/Discovery/DeviceMatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ enum DeviceMatch {
return matchesKnownHostname(serviceName)
}

/// The FALLBACK. A heuristic, and deliberately labelled as one — it exists to keep un-updated
/// The FALLBACK. A heuristic, and deliberately labeled as one — it exists to keep un-updated
/// units working, not because it is correct.
///
/// The firmware advertises generic `_http._tcp` with no TXT record, so the hostname is all a
Expand Down
4 changes: 2 additions & 2 deletions Sources/KitchenSyncApp/Discovery/DiscoveryStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ enum DiscoveryStatus: Equatable {
/// The browser died. `NWBrowser` does not come back from `.failed` on its own —
/// somebody has to restart it.
case failed
/// Cancelled, because we asked.
/// Canceled, because we asked.
case stopped

/// `kDNSServiceErr_PolicyDenied`. The one signal that means "the user has to act".
Expand All @@ -42,7 +42,7 @@ enum DiscoveryStatus: Equatable {
self = .starting
case .ready:
self = .browsing
case .cancelled:
case .cancelled: // spelling: ok (NWBrowser.State, Apple API)
self = .stopped
case .waiting(let error):
// A permission denial ARRIVES AS A WAIT, which is precisely the trap: it looks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ final class KitchenSyncDiscovery {
}

func stop() {
browser?.stateUpdateHandler = nil // cancelling is not news; don't report .stopped
browser?.stateUpdateHandler = nil // canceling is not news; don't report .stopped
browser?.cancel()
browser = nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/KitchenSyncApp/Discovery/SetupNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum SetupNetwork {
/// - `KitchenSync-Setup` — the P4 (`wifi_link.c` `AP_SSID`)
/// - `KSTouch-Config` — the Touch (`KitchenSyncTouch.ino` `WiFi.softAP`)
/// Matched as a **family of prefixes**, not one string, so a new product's setup AP is
/// recognised without an app change — the same reason `DeviceMatch` matches host prefixes.
/// recognized without an app change — the same reason `DeviceMatch` matches host prefixes.
static let ssidPrefixes = ["kitchensync-setup", "kstouch-config"]

/// True if `ssid` is one of our devices' setup APs. Case-insensitive: a WiFi SSID's case is
Expand Down
15 changes: 15 additions & 0 deletions Sources/KitchenSyncApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
Expand All @@ -26,5 +28,18 @@
<string>KitchenSync finds your devices on the local network to show their status and control their MIDI clock.</string>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
2 changes: 1 addition & 1 deletion Sources/KitchenSyncApp/Models/TransportLaunchState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// Mirrors `TransportLaunchState` (`link-devices/X32Link/transport_launch.h`) as
/// reported per output in `/status`'s `launch[4]` array. Quantized: Play arms,
/// waits for the next bar line, then runs — Stop is immediate. That asymmetry is
/// deliberate (it's Ableton's own launch behaviour), so drive UI straight from
/// deliberate (it's Ableton's own launch behavior), so drive UI straight from
/// this value rather than re-deriving an "armed" state on the client — the
/// device already computes and reports it every poll.
enum TransportLaunchState: Int, Decodable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class DeviceDetailViewModel: ObservableObject {
func start() {
guard pollTask == nil else { return }
pollTask = Task { [weak self] in
while !Task.isCancelled {
while !Task.isCancelled { // spelling: ok (Apple API)
await self?.refreshStatus()
try? await Task.sleep(for: self?.pollInterval ?? .seconds(1))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ final class DeviceListViewModel: ObservableObject {
}
discovery.start()
pollTask = Task {
while !Task.isCancelled {
while !Task.isCancelled { // spelling: ok (Apple API)
await refreshAll()
try? await Task.sleep(for: .seconds(2))
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/KitchenSyncApp/Views/DeviceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ private struct DeviceRow: View {
// "0 PEERS" for a device that isn't answering is a lie dressed as
// a measurement. If there's no status, we don't know the peer count.
// And "NO SIGNAL" is wrong for a device that's answering fine — it's
// signalling perfectly, in a dialect we can't read.
// signaling perfectly, in a dialect we can't read.
Text(peersCaption)
.font(.ksMono(10.5))
.tracking(1.6)
Expand Down
6 changes: 3 additions & 3 deletions Sources/KitchenSyncApp/Views/DeviceLiveSettingsSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
/// **No surface ever mixes live and reboot controls.** The device's own web page puts the metronome
/// ENABLE toggle and the metro-accent toggle forty pixels apart, as the identical component, in the
/// same box — one applies instantly and the other restarts the device mid-set, with nothing on
/// screen to tell them apart. Better labelling would not have fixed that. Not being adjacent does.
/// screen to tell them apart. Better labeling would not have fixed that. Not being adjacent does.
///
/// Sections appear only for hardware the device actually reports (`link-devices` ESP-030): no
/// speaker, no click section; no strip, no LED section. Solder a strip onto a Touch and flip one
Expand Down Expand Up @@ -162,7 +162,7 @@ struct DeviceLiveSettingsSheet: View {
}

KSField(prefix: "BEAT") {
ColorPicker("Beat colour", selection: Binding(
ColorPicker("Beat color", selection: Binding(
get: { Color(hex: l.beatColor) },
set: { onEdit(.ledBeatColor($0.rgb24)) }
), supportsOpacity: false)
Expand All @@ -171,7 +171,7 @@ struct DeviceLiveSettingsSheet: View {
}

KSField(prefix: "ACCENT") {
ColorPicker("Accent colour", selection: Binding(
ColorPicker("Accent color", selection: Binding(
get: { Color(hex: l.accentColor) },
set: { onEdit(.ledAccentColor($0.rgb24)) }
), supportsOpacity: false)
Expand Down
6 changes: 3 additions & 3 deletions Sources/KitchenSyncApp/Views/FirmwareUpdateSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import UniformTypeIdentifiers
/// the device — the firmware doesn't expose its chip target, so the app cannot check.
/// A filename convention is not a safety mechanism, but an unlabelled "Flash" button
/// is worse. So the confirmation names the device and requires an explicit
/// acknowledgement of the target. The proper fix is firmware-side (expose the target
/// acknowledgment of the target. The proper fix is firmware-side (expose the target
/// in /status or /update) and belongs with T-011.
///
/// The reassurance is real and worth stating plainly: dual-slot OTA (P4-017) means a
Expand Down Expand Up @@ -40,7 +40,7 @@ struct FirmwareUpdateSheet: View {
picker

if binary != nil {
targetAcknowledgement
targetAcknowledgment
}

if let failure {
Expand Down Expand Up @@ -124,7 +124,7 @@ struct FirmwareUpdateSheet: View {

/// The app cannot verify the chip target, so the user must. Not a rubber stamp —
/// it's the only gate that exists.
private var targetAcknowledgement: some View {
private var targetAcknowledgment: some View {
HStack(alignment: .top, spacing: 10) {
Toggle("", isOn: $acknowledgedTarget)
.toggleStyle(KSSwitchStyle())
Expand Down
4 changes: 2 additions & 2 deletions Sources/KitchenSyncApp/Views/KSControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ struct KSLiveSlider: View {
debounce?.cancel()
debounce = Task {
try? await Task.sleep(for: .milliseconds(60))
guard !Task.isCancelled else { return }
guard !Task.isCancelled else { return } // spelling: ok (Apple API)
onCommit(Int(new.rounded()))
}
}
Expand All @@ -234,7 +234,7 @@ struct KSLiveSlider: View {
// Let the device win once it has answered — never leave a slider showing a
// number the device doesn't have.
.onChange(of: value) { _, new in
if debounce?.isCancelled ?? true { local = nil }
if debounce?.isCancelled ?? true { local = nil } // spelling: ok (Apple API)
_ = new
}
.accessibilityLabel(label)
Expand Down
6 changes: 3 additions & 3 deletions Sources/KitchenSyncApp/Views/KSGlass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SwiftUI
/// The web UI draws a 7-segment display with an all-segments-lit "188.8" ghost
/// behind the live value, imitating the unlit segments of a real LED display.
/// That trick REQUIRES the DSEG7 Classic face. It isn't bundled yet, so per the
/// design direction the ghost layer is deliberately DROPPED: a grey duplicate of
/// design direction the ghost layer is deliberately DROPPED: a gray duplicate of
/// a non-segmented font doesn't read as unlit segments, it reads as a rendering
/// bug. Bundle DSEG7 and the ghost comes back — see `ghost` below.
struct KSGlass: View {
Expand Down Expand Up @@ -45,10 +45,10 @@ struct KSGlass: View {
}
}

/// The `.pwr` LED — breathes while the device is answering, goes dead grey when
/// The `.pwr` LED — breathes while the device is answering, goes dead gray when
/// it isn't. The web page never needs an offline state; the app needs one
/// constantly, and it's the first thing a user checks. (Proper unreachable
/// modelling — consecutive-failure thresholds — is T-010.)
/// modeling — consecutive-failure thresholds — is T-010.)
struct KSPowerLED: View {
let alive: Bool

Expand Down
2 changes: 1 addition & 1 deletion Tests/KitchenSyncAppTests/DeviceListViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ final class DeviceListViewModelTests: XCTestCase {

/// One dropped request on a busy LAN is NOISE, not a disconnection. At a 2s poll,
/// flapping a red badge on every missed packet would be worse than the old
/// behaviour. A single miss keeps the device reachable and keeps its last status.
/// behavior. A single miss keeps the device reachable and keeps its last status.
func test_a_single_missed_poll_does_not_declare_the_device_gone() async {
StubURLProtocol.reset()
StubURLProtocol.routes["GET /status"] = .init(body: Self.statusJSON)
Expand Down
2 changes: 1 addition & 1 deletion Tests/KitchenSyncAppTests/DeviceMatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class DeviceMatchTests: XCTestCase {
/// and X32Link use. hasPrefix is case-sensitive, so `"KitchenSync".hasPrefix(
/// "kitchensync")` is false and the P4 was invisible. A Bonjour name's case is not
/// meaningful; the match must be case-insensitive.
func test_the_p4_is_matched_despite_a_capitalised_instance_name() {
func test_the_p4_is_matched_despite_a_capitalized_instance_name() {
XCTAssertTrue(DeviceMatch.isKitchenSync(serviceName: "KitchenSync", txt: nil))
XCTAssertTrue(DeviceMatch.isKitchenSync(serviceName: "KSTouch-DFD0", txt: nil))
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/KitchenSyncAppTests/DiscoveryStatusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ final class DiscoveryStatusTests: XCTestCase {
XCTAssertEqual(DiscoveryStatus(browserState: .failed(.posix(.ENETDOWN))), .failed)
}

func test_setup_and_cancelled_are_not_errors() {
func test_setup_and_canceled_are_not_errors() {
XCTAssertEqual(DiscoveryStatus(browserState: .setup), .starting)
XCTAssertEqual(DiscoveryStatus(browserState: .cancelled), .stopped)
XCTAssertEqual(DiscoveryStatus(browserState: .cancelled), .stopped) // spelling: ok (NWBrowser.State, Apple API)
}

/// Only `permissionDenied` is the user's to fix. Everything else is ours or the
Expand Down
8 changes: 4 additions & 4 deletions Tests/KitchenSyncAppTests/RealTouchStatusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ final class RealTouchStatusTests: XCTestCase {
/// THE BUG, reduced to one assertion.
///
/// `link_owns` is a LIVE claim. `ks_status.h`: "when link_owns is true the manual
/// PLAY/STOP buttons are ignored, so the UI greys them". The app obeys that contract
/// PLAY/STOP buttons are ignored, so the UI grays them". The app obeys that contract
/// — `TransportAppearance.acceptsTap = !linkOwned` — and so it refused to even SEND
/// the tap.
///
Expand All @@ -107,7 +107,7 @@ final class RealTouchStatusTests: XCTestCase {
linkOwned: s.linkOwnsTransport)

XCTAssertTrue(button.acceptsTap,
"the device obeys play/stop — a UI that greys the button is lying to the user")
"the device obeys play/stop — a UI that grays the button is lying to the user")
}

/// ...and it must be able to stop it again.
Expand All @@ -127,14 +127,14 @@ final class RealTouchStatusTests: XCTestCase {
/// then `link_owns:true` is true, its manual buttons really are ignored, and greying
/// them is correct: a button that does nothing is worse than a button that says so.
/// The Touch's mistake was CLAIMING that while obeying anyway.
func test_a_device_that_really_does_defer_to_link_still_greys_the_button() throws {
func test_a_device_that_really_does_defer_to_link_still_grays_the_button() throws {
let s = try JSONDecoder().decode(KsStatus.self, from: Self.lyingLinkOwnsJSON)

let button = TransportAppearance.appearance(for: s.launch[0],
linkOwned: s.linkOwnsTransport)

XCTAssertFalse(button.acceptsTap,
"link_owns is a LIVE claim; honouring it is right. The fix belongs in whoever LIES.")
"link_owns is a LIVE claim; honoring it is right. The fix belongs in whoever LIES.")
}

/// The Touch has no Link-phase block (`xf*`), so `phaseHealth` must be nil rather
Expand Down
4 changes: 2 additions & 2 deletions Tests/KitchenSyncAppTests/SetupNetworkTests.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import XCTest
@testable import KitchenSyncApp

/// T-026: a device in SoftAP setup mode isn't on the LAN, so the app recognises it by its
/// T-026: a device in SoftAP setup mode isn't on the LAN, so the app recognizes it by its
/// setup SSID (to guide/join the AP) and reaches it at the fixed setup address.
final class SetupNetworkTests: XCTestCase {
func test_the_fleet_setup_aps_are_recognised() {
func test_the_fleet_setup_aps_are_recognized() {
XCTAssertTrue(SetupNetwork.isSetupAP(ssid: "KitchenSync-Setup")) // the P4
XCTAssertTrue(SetupNetwork.isSetupAP(ssid: "KSTouch-Config")) // the Touch
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/KitchenSyncAppTests/TransportAppearanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class TransportAppearanceTests: XCTestCase {
let a = TransportAppearance.appearance(for: .stopped, linkOwned: false)

XCTAssertEqual(a.label, "STOPPED")
// Ember, not grey: grey is what DISABLED looks like, and a stopped
// Ember, not gray: gray is what DISABLED looks like, and a stopped
// output is the opposite of disabled — it's loaded and ready to fire.
XCTAssertEqual(a.face, .ember)
XCTAssertFalse(a.blinks)
Expand All @@ -27,7 +27,7 @@ final class TransportAppearanceTests: XCTestCase {
/// The one that matters. Play is quantized — a tap arms the output and the
/// bar line may be most of a bar away. The blink is the ONLY evidence the
/// tap registered. The device's own web UI gets this wrong: its `.tgl.arming`
/// is a static amber with a 150ms colour transition and no motion at all.
/// is a static amber with a 150ms color transition and no motion at all.
func test_armed_is_amber_and_blinks() {
let a = TransportAppearance.appearance(for: .armed, linkOwned: false)

Expand Down
2 changes: 1 addition & 1 deletion Tests/KitchenSyncAppTests/UnsupportedDeviceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final class UnsupportedDeviceTests: XCTestCase {
XCTAssertEqual(vm.reachability(of: "kstouch-dfd0.local"), .unsupported)
}

/// And the T-010 behaviour still holds: a device that genuinely stops answering IS unreachable.
/// And the T-010 behavior still holds: a device that genuinely stops answering IS unreachable.
func test_a_device_that_stops_answering_is_still_unreachable() async {
StubURLProtocol.routes["GET /status"] = .init(body: Self.realTouchStatusJSON)
let vm = makeListVM()
Expand Down
2 changes: 1 addition & 1 deletion Tools/make_app_icon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ ctx.setMiterLimit(10)
// concentric banding, a contour map. Neither is a glow, because a glow FALLS OFF and
// a stroke does not.
//
// A shadow does. Zero offset + a blur radius + the LED colour is a real Gaussian
// A shadow does. Zero offset + a blur radius + the LED color is a real Gaussian
// bloom around the path. Two passes, because one is too timid to see on charcoal.
for (blur, alpha) in [(S * 0.075, 0.75), (S * 0.030, 0.85)] as [(Double, Double)] {
ctx.saveGState()
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Models (pure) KsStatus, KsConfig, KsLiveEdit, TransportLaunchStat
KsStatusTempoSource, TransportAppearance, DeviceMatch …
```

- **Models are pure and testable.** State→appearance mapping, tempo-source labelling,
- **Models are pure and testable.** State→appearance mapping, tempo-source labeling,
transport summary, and device-match logic are pure functions with unit tests —
never buried in a view.
- **The firmware is the source of truth for the models.** Field names, value grammars,
Expand Down
Loading
Loading