diff --git a/TODO.md b/TODO.md index 2bd65e6..87a1c3d 100644 --- a/TODO.md +++ b/TODO.md @@ -16,8 +16,6 @@ # M8 — macOS client -- macOS client strict memory safety - Turn on `.strictMemorySafety()` for the `Byre` target in `mac/Package.swift` and resolve what it flags (`@unchecked Sendable` on `Re`, `MainActor.assumeIsolated` in the key and click monitors, and anything else), so `swift build` is warning-free with it on. - macOS client signing and SwiftData with iCloud sync Sign `byre.app` with the paid developer team (provisioning profile, iCloud and CloudKit entitlements) in `mac/bundle.sh`, and move the app's persisted state (the `byre.server` origin, drafts, last visits) from `UserDefaults` to SwiftData with CloudKit sync, so the server address follows the user to every Mac. - macOS client parity review diff --git a/mac/Package.swift b/mac/Package.swift index 86a30d7..6867bf9 100644 --- a/mac/Package.swift +++ b/mac/Package.swift @@ -25,7 +25,8 @@ let package = Package( .product(name: "SwiftTerm", package: "SwiftTerm"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), - ] + ], + swiftSettings: [.strictMemorySafety()] ), ] ) diff --git a/mac/Sources/Byre/Diagram.swift b/mac/Sources/Byre/Diagram.swift index 92d1a03..a045900 100644 --- a/mac/Sources/Byre/Diagram.swift +++ b/mac/Sources/Byre/Diagram.swift @@ -277,8 +277,13 @@ struct DiagramLayout { let size: CGSize } -nonisolated(unsafe) private let labelFont = Fonts.ct(13) -nonisolated(unsafe) private let edgeFont = Fonts.ct(12) +private var labelFont: CTFont { + Fonts.ns(13) as CTFont +} + +private var edgeFont: CTFont { + Fonts.ns(12) as CTFont +} func textSize(_ text: String, _ font: CTFont) -> CGSize { let lines = text.components(separatedBy: "\n") diff --git a/mac/Sources/Byre/Files.swift b/mac/Sources/Byre/Files.swift index bcca51e..6adb69c 100644 --- a/mac/Sources/Byre/Files.swift +++ b/mac/Sources/Byre/Files.swift @@ -26,9 +26,9 @@ private func bytes(_ n: Int64) -> String { return "\(n) B" } if n < 1024 * 1024 { - return String(format: "%.1f KiB", Double(n) / 1024) + return "\((Double(n) / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) KiB" } - return String(format: "%.1f MiB", Double(n) / 1024 / 1024) + return "\((Double(n) / 1024 / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) MiB" } private func shown(_ c: FileContent) -> Int64 { diff --git a/mac/Sources/Byre/Fonts.swift b/mac/Sources/Byre/Fonts.swift index 44be0b2..0eb263d 100644 --- a/mac/Sources/Byre/Fonts.swift +++ b/mac/Sources/Byre/Fonts.swift @@ -1,4 +1,5 @@ import CoreText +import Synchronization import SwiftUI enum Fonts { @@ -14,7 +15,7 @@ enum Fonts { }() private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor { - let d = descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]! + let d = unsafe descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]! return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight) } @@ -24,29 +25,25 @@ enum Fonts { return CTFontCreateWithFontDescriptor(d, size, nil) } - nonisolated(unsafe) private static var fonts: [String: (font: Font, ns: NSFont)] = [:] - private static let lock = NSLock() + private static let fonts = Mutex<[String: CTFont]>([:]) - private static func cached(_ size: CGFloat, _ weight: CGFloat, _ italic: Bool) -> (font: Font, ns: NSFont) { + private static func cached(_ size: CGFloat, _ weight: CGFloat, _ italic: Bool) -> CTFont { let key = "\(size) \(weight) \(italic)" - lock.lock() - defer { - lock.unlock() - } - if let f = fonts[key] { + return fonts.withLock { fonts in + if let f = fonts[key] { + return f + } + let f = ct(size, weight: weight, italic: italic) + fonts[key] = f return f } - let c = ct(size, weight: weight, italic: italic) - let f = (Font(c), c as NSFont) - fonts[key] = f - return f } static func font(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> Font { - cached(size, weight, italic).font + Font(cached(size, weight, italic)) } static func ns(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> NSFont { - cached(size, weight, italic).ns + cached(size, weight, italic) as NSFont } } diff --git a/mac/Sources/Byre/MarkdownView.swift b/mac/Sources/Byre/MarkdownView.swift index 9d8f7f2..abf7928 100644 --- a/mac/Sources/Byre/MarkdownView.swift +++ b/mac/Sources/Byre/MarkdownView.swift @@ -112,7 +112,7 @@ struct InlineRenderer { private func run(_ s: String, _ style: Style) -> AttributedString { var a = AttributedString(s) a.font = Fonts.font(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic) - a.appKit.font = Fonts.ns(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic) + a.mergeAttributes(try! AttributeContainer([.font: Fonts.ns(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic)], including: \.appKit)) if let link = style.link { a.link = link } @@ -364,7 +364,7 @@ private struct FlowBuilder { s.append(NSAttributedString(string: "\n", attributes: [.font: Fonts.ns(ctx.size)])) let all = NSRange(location: 0, length: s.length) let color = NSColor(ctx.color) - s.enumerateAttribute(.foregroundColor, in: all) { value, range, _ in + unsafe s.enumerateAttribute(.foregroundColor, in: all) { value, range, _ in if value == nil { s.addAttribute(.foregroundColor, value: color, range: range) } @@ -405,8 +405,8 @@ private struct FlowText: NSViewRepresentable { v.isVerticallyResizable = false v.isHorizontallyResizable = false v.textContainerInset = .zero - v.textContainer!.lineFragmentPadding = 0 - v.textContainer!.widthTracksTextView = true + unsafe v.textContainer!.lineFragmentPadding = 0 + unsafe v.textContainer!.widthTracksTextView = true v.linkTextAttributes = [.cursor: NSCursor.pointingHand] v.delegate = context.coordinator return v @@ -414,11 +414,11 @@ private struct FlowText: NSViewRepresentable { func updateNSView(_ v: FlowTextView, context: Context) { context.coordinator.openURL = context.environment.openURL - if v.textStorage!.isEqual(to: text) { + if unsafe v.textStorage!.isEqual(to: text) { return } let selected = v.selectedRanges - v.textStorage!.setAttributedString(text) + unsafe v.textStorage!.setAttributedString(text) if selected.allSatisfy({ NSMaxRange($0.rangeValue) <= text.length }) { v.selectedRanges = selected } diff --git a/mac/Sources/Byre/PaneFocus.swift b/mac/Sources/Byre/PaneFocus.swift index e3639af..7bd5b36 100644 --- a/mac/Sources/Byre/PaneFocus.swift +++ b/mac/Sources/Byre/PaneFocus.swift @@ -166,7 +166,7 @@ struct WindowReader: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { - if let w = view.window { + if let w = unsafe view.window { found(w) } } @@ -175,7 +175,7 @@ struct WindowReader: NSViewRepresentable { func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { - if let w = nsView.window { + if let w = unsafe nsView.window { found(w) } } diff --git a/mac/Sources/Byre/Status.swift b/mac/Sources/Byre/Status.swift index 3ce9361..be04d90 100644 --- a/mac/Sources/Byre/Status.swift +++ b/mac/Sources/Byre/Status.swift @@ -30,7 +30,7 @@ private func bytes(_ n: Int64) -> String { v /= 1024 i += 1 } - return String(format: i == 0 ? "%.0f %@" : "%.1f %@", v, units[i]) + return "\(v.formatted(.number.precision(.fractionLength(i == 0 ? 0 : 1)).grouping(.never))) \(units[i])" } private func resetText(_ unixSeconds: Double) -> String { diff --git a/mac/Sources/Byre/Terminal.swift b/mac/Sources/Byre/Terminal.swift index d020e71..541af29 100644 --- a/mac/Sources/Byre/Terminal.swift +++ b/mac/Sources/Byre/Terminal.swift @@ -82,7 +82,7 @@ struct TerminalHost: NSViewRepresentable { context.coordinator.view = t DispatchQueue.main.async { colour(t) - t.window?.makeFirstResponder(t) + unsafe t.window?.makeFirstResponder(t) context.coordinator.connect() } return t