dsh-notch 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BASELINE.md +13 -0
- package/DESIGN.md +34 -0
- package/LICENSE +21 -0
- package/README.en.md +136 -0
- package/README.md +145 -0
- package/cordis.yml +5 -0
- package/docs/motion-audit.md +65 -0
- package/docs/motion-continuity.md +77 -0
- package/docs/releases/v0.3.0.md +20 -0
- package/dshx.yml +14 -0
- package/macos/Package.swift +17 -0
- package/macos/STATUS-MOTION.md +49 -0
- package/macos/Sources/Client.swift +139 -0
- package/macos/Sources/IdleRobot.swift +423 -0
- package/macos/Sources/NotchMarkdown.swift +266 -0
- package/macos/Sources/Panel.swift +153 -0
- package/macos/Sources/Resources/Idle/balance.json +1 -0
- package/macos/Sources/Resources/Idle/blink.json +1 -0
- package/macos/Sources/Resources/Idle/cube-in.json +1 -0
- package/macos/Sources/Resources/Idle/dance.json +1 -0
- package/macos/Sources/Resources/Idle/hop.json +1 -0
- package/macos/Sources/Resources/Idle/nod.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-in.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-out.json +1 -0
- package/macos/Sources/Resources/Idle/scan.json +1 -0
- package/macos/Sources/Resources/Idle/sleep.json +1 -0
- package/macos/Sources/Resources/Idle/sneeze.json +1 -0
- package/macos/Sources/Resources/Idle/stretch.json +1 -0
- package/macos/Sources/Resources/Idle/tilt.json +1 -0
- package/macos/Sources/ReviewStudio.swift +337 -0
- package/macos/Sources/RootView.swift +1093 -0
- package/macos/Sources/StatusOrbit.swift +642 -0
- package/macos/Sources/main.swift +153 -0
- package/macos/Tests/ExpandedHeightProbe.swift +108 -0
- package/macos/Tests/GeometryProbe.swift +98 -0
- package/macos/Tests/IdleProbe.swift +294 -0
- package/macos/Tests/MotionProbe.swift +106 -0
- package/macos/Tests/OutcomeProbe.swift +133 -0
- package/macos/Tests/ScrollbarProbe.swift +91 -0
- package/macos/Tests/expanded-height.sh +13 -0
- package/macos/Tests/geometry.sh +14 -0
- package/macos/Tests/idle.sh +14 -0
- package/macos/Tests/motion.sh +14 -0
- package/macos/Tests/outcome.sh +14 -0
- package/macos/Tests/scrollbar.sh +13 -0
- package/package.json +46 -0
- package/src/board.ts +299 -0
- package/src/browse-sync.ts +59 -0
- package/src/dsh-notch.ts +60 -0
- package/src/http.ts +215 -0
- package/src/session-state.ts +31 -0
- package/src/store.ts +49 -0
- package/src/types.ts +65 -0
- package/tests/answer-lifecycle.test.mjs +23 -0
- package/tests/board.test.mjs +7 -0
- package/tests/browse-sync.test.mjs +20 -0
- package/tests/mirror.test.mjs +4 -0
- package/tests/peer-range.test.mjs +21 -0
- package/tests/session-state.test.mjs +54 -0
- package/tests/unread-hold.test.mjs +90 -0
- package/tools/desktop-shell/README.md +56 -0
- package/tools/desktop-shell/child-log.mjs +6 -0
- package/tools/desktop-shell/child-log.test.mjs +14 -0
- package/tools/desktop-shell/package-recovery.py +78 -0
- package/tools/desktop-shell/recovery-probe.cjs +50 -0
- package/tools/desktop-shell/renderer-recovery.mjs +95 -0
- package/tools/desktop-shell/renderer-recovery.test.mjs +24 -0
- package/tools/diagnostics/completion-watch.py +101 -0
- package/tools/diagnostics/completion-watch.test.py +17 -0
- package/tools/diagnostics/renderer-diagnostics.mjs +56 -0
- package/tools/diagnostics/renderer-diagnostics.test.mjs +32 -0
- package/tools/idle/LICENSE.open-bot-motion +21 -0
- package/tools/idle/export.mjs +40 -0
- package/tools/idle/index.html +4 -0
- package/tools/idle/motions.js +83 -0
- package/tools/idle/original.js +1673 -0
- package/tools/idle/renderer.js +1673 -0
- package/tools/recording/Client.swift +76 -0
- package/tools/recording/DemoCatalog.swift +174 -0
- package/tools/recording/README.md +23 -0
- package/tools/recording/build.sh +41 -0
- package/tools/recording/main.swift +100 -0
- package/tools/review/ReviewStudio.swift +236 -0
- package/tools/review/audit.sh +19 -0
- package/tools/review/build.sh +7 -0
- package/tools/review/main.swift +158 -0
- package/tools/review/render-audit.swift +32 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Block Markdown using DSH body type: 14pt system, 12pt SF Mono.
|
|
5
|
+
enum NotchMarkdown {
|
|
6
|
+
static let bodySize: CGFloat = 14
|
|
7
|
+
static let headingSize: CGFloat = 17
|
|
8
|
+
static let codeSize: CGFloat = 12
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct NotchMarkdownView: View {
|
|
12
|
+
let source: String
|
|
13
|
+
|
|
14
|
+
var body: some View {
|
|
15
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
16
|
+
ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in
|
|
17
|
+
blockView(block)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private var blocks: [Block] { Block.parse(source) }
|
|
24
|
+
|
|
25
|
+
@ViewBuilder
|
|
26
|
+
private func blockView(_ block: Block) -> some View {
|
|
27
|
+
switch block {
|
|
28
|
+
case .heading(let level, let text):
|
|
29
|
+
inline(text)
|
|
30
|
+
.font(.system(size: level <= 1 ? 18 : level == 2 ? 16 : 15, weight: .semibold))
|
|
31
|
+
.foregroundStyle(.white)
|
|
32
|
+
.padding(.top, level <= 2 ? 8 : 4)
|
|
33
|
+
case .paragraph(let text):
|
|
34
|
+
inline(text)
|
|
35
|
+
.font(.system(size: NotchMarkdown.bodySize))
|
|
36
|
+
.foregroundStyle(Color.white.opacity(0.9))
|
|
37
|
+
.lineSpacing(6)
|
|
38
|
+
case .list(let items, let ordered):
|
|
39
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
40
|
+
ForEach(Array(items.enumerated()), id: \.offset) { i, item in
|
|
41
|
+
HStack(alignment: .top, spacing: 8) {
|
|
42
|
+
Text(ordered ? "\(i + 1)." : "•")
|
|
43
|
+
.font(.system(size: NotchMarkdown.bodySize, weight: .medium))
|
|
44
|
+
.foregroundStyle(Color.white.opacity(0.55))
|
|
45
|
+
.frame(width: 18, alignment: .trailing)
|
|
46
|
+
inline(item)
|
|
47
|
+
.font(.system(size: NotchMarkdown.bodySize))
|
|
48
|
+
.foregroundStyle(Color.white.opacity(0.9))
|
|
49
|
+
.lineSpacing(5)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
case .table(let headers, let rows):
|
|
54
|
+
table(headers: headers, rows: rows)
|
|
55
|
+
case .code(let text):
|
|
56
|
+
Text(text)
|
|
57
|
+
.font(.system(size: NotchMarkdown.codeSize, design: .monospaced))
|
|
58
|
+
.foregroundStyle(Color.white.opacity(0.92))
|
|
59
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
60
|
+
.padding(10)
|
|
61
|
+
.background(
|
|
62
|
+
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
63
|
+
.fill(Color.white.opacity(0.07))
|
|
64
|
+
)
|
|
65
|
+
case .quote(let text):
|
|
66
|
+
inline(text)
|
|
67
|
+
.font(.system(size: NotchMarkdown.bodySize))
|
|
68
|
+
.foregroundStyle(Color.white.opacity(0.7))
|
|
69
|
+
.padding(.leading, 10)
|
|
70
|
+
.overlay(alignment: .leading) {
|
|
71
|
+
Rectangle().fill(Color.white.opacity(0.28)).frame(width: 2)
|
|
72
|
+
}
|
|
73
|
+
case .rule:
|
|
74
|
+
Divider().overlay(Color.white.opacity(0.18))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func inline(_ markdown: String) -> Text {
|
|
79
|
+
var options = AttributedString.MarkdownParsingOptions()
|
|
80
|
+
options.interpretedSyntax = .inlineOnlyPreservingWhitespace
|
|
81
|
+
options.failurePolicy = .returnPartiallyParsedIfPossible
|
|
82
|
+
if let parsed = try? AttributedString(markdown: markdown, options: options) {
|
|
83
|
+
var styled = parsed
|
|
84
|
+
for run in styled.runs {
|
|
85
|
+
if run.inlinePresentationIntent?.contains(.code) == true {
|
|
86
|
+
styled[run.range].font = .system(size: NotchMarkdown.codeSize, design: .monospaced)
|
|
87
|
+
styled[run.range].backgroundColor = NSColor.white.withAlphaComponent(0.1)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Text(styled)
|
|
91
|
+
}
|
|
92
|
+
return Text(markdown)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func table(headers: [String], rows: [[String]]) -> some View {
|
|
96
|
+
let columns = max(headers.count, rows.map(\.count).max() ?? 0)
|
|
97
|
+
return VStack(alignment: .leading, spacing: 0) {
|
|
98
|
+
if !headers.isEmpty {
|
|
99
|
+
tableRow(cells: headers, header: true, columns: columns)
|
|
100
|
+
Rectangle().fill(Color.white.opacity(0.2)).frame(height: 1)
|
|
101
|
+
}
|
|
102
|
+
ForEach(Array(rows.enumerated()), id: \.offset) { i, row in
|
|
103
|
+
tableRow(cells: row, header: false, columns: columns)
|
|
104
|
+
if i < rows.count - 1 {
|
|
105
|
+
Rectangle().fill(Color.white.opacity(0.08)).frame(height: 1)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
.padding(8)
|
|
110
|
+
.background(
|
|
111
|
+
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
112
|
+
.stroke(Color.white.opacity(0.16), lineWidth: 1)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func tableRow(cells: [String], header: Bool, columns: Int) -> some View {
|
|
117
|
+
HStack(alignment: .top, spacing: 0) {
|
|
118
|
+
ForEach(0..<columns, id: \.self) { i in
|
|
119
|
+
let cell = i < cells.count ? cells[i] : ""
|
|
120
|
+
Text(cell)
|
|
121
|
+
.font(.system(size: 12, weight: header ? .semibold : .regular))
|
|
122
|
+
.foregroundStyle(Color.white.opacity(header ? 0.95 : 0.82))
|
|
123
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
124
|
+
.padding(.vertical, 5)
|
|
125
|
+
.padding(.horizontal, 6)
|
|
126
|
+
if i < columns - 1 {
|
|
127
|
+
Rectangle().fill(Color.white.opacity(0.1)).frame(width: 1)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private enum Block {
|
|
135
|
+
case heading(Int, String)
|
|
136
|
+
case paragraph(String)
|
|
137
|
+
case list([String], Bool)
|
|
138
|
+
case table([String], [[String]])
|
|
139
|
+
case code(String)
|
|
140
|
+
case quote(String)
|
|
141
|
+
case rule
|
|
142
|
+
|
|
143
|
+
static func parse(_ source: String) -> [Block] {
|
|
144
|
+
let lines = source.replacingOccurrences(of: "\r\n", with: "\n").split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
|
|
145
|
+
var blocks: [Block] = []
|
|
146
|
+
var i = 0
|
|
147
|
+
while i < lines.count {
|
|
148
|
+
let raw = lines[i]
|
|
149
|
+
let line = raw.trimmingCharacters(in: .whitespaces)
|
|
150
|
+
if line.isEmpty { i += 1; continue }
|
|
151
|
+
if line.hasPrefix("```") {
|
|
152
|
+
i += 1
|
|
153
|
+
var body: [String] = []
|
|
154
|
+
while i < lines.count, !lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("```") {
|
|
155
|
+
body.append(lines[i])
|
|
156
|
+
i += 1
|
|
157
|
+
}
|
|
158
|
+
if i < lines.count { i += 1 }
|
|
159
|
+
blocks.append(.code(body.joined(separator: "\n")))
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if line == "---" || line == "***" || line == "___" {
|
|
163
|
+
blocks.append(.rule)
|
|
164
|
+
i += 1
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if let heading = heading(line) {
|
|
168
|
+
blocks.append(heading)
|
|
169
|
+
i += 1
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if line.hasPrefix("|") && i + 1 < lines.count, isTableDivider(lines[i + 1]) {
|
|
173
|
+
let headers = cells(line)
|
|
174
|
+
i += 2
|
|
175
|
+
var rows: [[String]] = []
|
|
176
|
+
while i < lines.count, lines[i].trimmingCharacters(in: .whitespaces).hasPrefix("|") {
|
|
177
|
+
rows.append(cells(lines[i]))
|
|
178
|
+
i += 1
|
|
179
|
+
}
|
|
180
|
+
blocks.append(.table(headers, rows))
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if line.hasPrefix("> ") || line == ">" {
|
|
184
|
+
var quoted: [String] = []
|
|
185
|
+
while i < lines.count {
|
|
186
|
+
let t = lines[i].trimmingCharacters(in: .whitespaces)
|
|
187
|
+
if t.hasPrefix("> ") { quoted.append(String(t.dropFirst(2))) }
|
|
188
|
+
else if t == ">" { quoted.append("") }
|
|
189
|
+
else { break }
|
|
190
|
+
i += 1
|
|
191
|
+
}
|
|
192
|
+
blocks.append(.quote(quoted.joined(separator: "\n")))
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if isBullet(line) || isOrdered(line) {
|
|
196
|
+
let ordered = isOrdered(line)
|
|
197
|
+
var items: [String] = []
|
|
198
|
+
while i < lines.count {
|
|
199
|
+
let t = lines[i].trimmingCharacters(in: .whitespaces)
|
|
200
|
+
if ordered, let item = orderedItem(t) { items.append(item) }
|
|
201
|
+
else if !ordered, let item = bulletItem(t) { items.append(item) }
|
|
202
|
+
else { break }
|
|
203
|
+
i += 1
|
|
204
|
+
}
|
|
205
|
+
blocks.append(.list(items, ordered))
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
var para: [String] = [line]
|
|
209
|
+
i += 1
|
|
210
|
+
while i < lines.count {
|
|
211
|
+
let t = lines[i].trimmingCharacters(in: .whitespaces)
|
|
212
|
+
if t.isEmpty || t.hasPrefix("#") || t.hasPrefix("|") || t.hasPrefix("```") || t.hasPrefix("> ") || isBullet(t) || isOrdered(t) || t == "---" { break }
|
|
213
|
+
para.append(t)
|
|
214
|
+
i += 1
|
|
215
|
+
}
|
|
216
|
+
blocks.append(.paragraph(para.joined(separator: " ")))
|
|
217
|
+
}
|
|
218
|
+
return blocks
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static func heading(_ line: String) -> Block? {
|
|
222
|
+
guard line.hasPrefix("#") else { return nil }
|
|
223
|
+
var level = 0
|
|
224
|
+
for ch in line {
|
|
225
|
+
if ch == "#" { level += 1 } else { break }
|
|
226
|
+
}
|
|
227
|
+
guard level > 0, level <= 6, line.count > level, line[line.index(line.startIndex, offsetBy: level)] == " " else { return nil }
|
|
228
|
+
return .heading(level, String(line.dropFirst(level + 1)))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private static func isTableDivider(_ line: String) -> Bool {
|
|
232
|
+
let t = line.trimmingCharacters(in: .whitespaces)
|
|
233
|
+
guard t.hasPrefix("|") else { return false }
|
|
234
|
+
return t.replacingOccurrences(of: "|", with: "").replacingOccurrences(of: ":", with: "").replacingOccurrences(of: "-", with: "").replacingOccurrences(of: " ", with: "").isEmpty
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private static func cells(_ line: String) -> [String] {
|
|
238
|
+
var parts = line.split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
|
|
239
|
+
if parts.first == "" { parts.removeFirst() }
|
|
240
|
+
if parts.last == "" { parts.removeLast() }
|
|
241
|
+
return parts
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static func isBullet(_ line: String) -> Bool {
|
|
245
|
+
line.hasPrefix("- ") || line.hasPrefix("* ")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static func bulletItem(_ line: String) -> String? {
|
|
249
|
+
if line.hasPrefix("- ") { return String(line.dropFirst(2)) }
|
|
250
|
+
if line.hasPrefix("* ") { return String(line.dropFirst(2)) }
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private static func isOrdered(_ line: String) -> Bool {
|
|
255
|
+
orderedItem(line) != nil
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private static func orderedItem(_ line: String) -> String? {
|
|
259
|
+
guard let dot = line.firstIndex(of: ".") else { return nil }
|
|
260
|
+
let n = line[..<dot]
|
|
261
|
+
guard !n.isEmpty, n.allSatisfy(\.isNumber) else { return nil }
|
|
262
|
+
let rest = line[line.index(after: dot)...]
|
|
263
|
+
guard rest.first == " " else { return nil }
|
|
264
|
+
return String(rest.dropFirst())
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
enum NotchGeometryAnimation {
|
|
5
|
+
static let animation = Animation.spring(duration: 0.4, bounce: 0.08)
|
|
6
|
+
static let duration: TimeInterval = 0.4
|
|
7
|
+
static func progress(_ t: Double) -> Double {
|
|
8
|
+
let x = min(1, max(0, t))
|
|
9
|
+
return x*x*x*(x*(6*x-15)+10)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
final class NotchPanel: NSPanel {
|
|
14
|
+
private var resizeTimer: Timer?
|
|
15
|
+
private var resizeTarget: NSSize?
|
|
16
|
+
private var resizeGeneration = 0
|
|
17
|
+
|
|
18
|
+
func cancelResize() {
|
|
19
|
+
resizeTimer?.invalidate()
|
|
20
|
+
resizeTimer = nil
|
|
21
|
+
resizeTarget = nil
|
|
22
|
+
resizeGeneration += 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Animate one native rectangle, preserving its top-right edge at every frame.
|
|
26
|
+
func resizeAnchored(to size: NSSize, animated: Bool = true) {
|
|
27
|
+
guard resizeTarget != size else { return }
|
|
28
|
+
cancelResize()
|
|
29
|
+
let generation = resizeGeneration
|
|
30
|
+
resizeTarget = size
|
|
31
|
+
let start = frame
|
|
32
|
+
let end = NSRect(x: start.maxX - size.width, y: start.maxY - size.height, width: size.width, height: size.height)
|
|
33
|
+
guard animated, !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion else {
|
|
34
|
+
setFrame(end, display: true)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if #available(macOS 15.0, *) {
|
|
38
|
+
NSAnimationContext.animate(NotchGeometryAnimation.animation) {
|
|
39
|
+
self.animator().setFrame(end, display: true)
|
|
40
|
+
}
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
let began = ProcessInfo.processInfo.systemUptime
|
|
44
|
+
resizeTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in
|
|
45
|
+
Task { @MainActor in
|
|
46
|
+
guard let self, self.resizeGeneration == generation else { return }
|
|
47
|
+
let t = min(1, (ProcessInfo.processInfo.systemUptime - began) / NotchGeometryAnimation.duration)
|
|
48
|
+
let progress = NotchGeometryAnimation.progress(t)
|
|
49
|
+
let width = start.width + (size.width - start.width) * progress
|
|
50
|
+
let height = start.height + (size.height - start.height) * progress
|
|
51
|
+
self.setFrame(NSRect(x: start.maxX - width, y: start.maxY - height, width: width, height: height), display: true)
|
|
52
|
+
if t >= 1 { self.resizeTimer?.invalidate(); self.resizeTimer = nil }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override var canBecomeKey: Bool { true }
|
|
58
|
+
override var canBecomeMain: Bool { false }
|
|
59
|
+
|
|
60
|
+
// An accessory panel has no active app Edit menu to route Command keys.
|
|
61
|
+
// Handle only standard editing commands, and only for our focused editor.
|
|
62
|
+
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
|
63
|
+
guard isKeyWindow, let editor = firstResponder as? NSTextView else {
|
|
64
|
+
return super.performKeyEquivalent(with: event)
|
|
65
|
+
}
|
|
66
|
+
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
|
67
|
+
guard flags.contains(.command), !flags.contains(.control), !flags.contains(.option) else {
|
|
68
|
+
return super.performKeyEquivalent(with: event)
|
|
69
|
+
}
|
|
70
|
+
let key = event.charactersIgnoringModifiers?.lowercased()
|
|
71
|
+
if flags.contains(.shift) {
|
|
72
|
+
if key == "z", editor.isEditable, let undo = editor.undoManager, undo.canRedo {
|
|
73
|
+
undo.redo()
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
return super.performKeyEquivalent(with: event)
|
|
77
|
+
}
|
|
78
|
+
switch key {
|
|
79
|
+
case "a": editor.selectAll(nil)
|
|
80
|
+
case "c": editor.copy(nil)
|
|
81
|
+
case "x" where editor.isEditable: editor.cut(nil)
|
|
82
|
+
case "v" where editor.isEditable: editor.paste(nil)
|
|
83
|
+
case "z" where editor.isEditable:
|
|
84
|
+
guard let undo = editor.undoManager, undo.canUndo else {
|
|
85
|
+
return super.performKeyEquivalent(with: event)
|
|
86
|
+
}
|
|
87
|
+
undo.undo()
|
|
88
|
+
default: return super.performKeyEquivalent(with: event)
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
convenience init(size: NSSize) {
|
|
94
|
+
self.init(
|
|
95
|
+
contentRect: NSRect(origin: .zero, size: size),
|
|
96
|
+
styleMask: [.borderless, .nonactivatingPanel],
|
|
97
|
+
backing: .buffered,
|
|
98
|
+
defer: false
|
|
99
|
+
)
|
|
100
|
+
level = .statusBar
|
|
101
|
+
collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
|
102
|
+
isOpaque = false
|
|
103
|
+
backgroundColor = .clear
|
|
104
|
+
appearance = NSAppearance(named: .darkAqua)
|
|
105
|
+
hasShadow = false
|
|
106
|
+
isMovable = false
|
|
107
|
+
hidesOnDeactivate = false
|
|
108
|
+
becomesKeyOnlyIfNeeded = true
|
|
109
|
+
isReleasedWhenClosed = false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Put the SwiftUI host on top of a real window-backed HUD blur.
|
|
113
|
+
/// A VisualEffect inside NSHostingView is covered by the hosting view's opaque fill.
|
|
114
|
+
func embedHost(_ hosting: NSView) {
|
|
115
|
+
let effect = NSVisualEffectView(frame: contentView?.bounds ?? NSRect(origin: .zero, size: frame.size))
|
|
116
|
+
effect.material = .hudWindow
|
|
117
|
+
effect.blendingMode = .behindWindow
|
|
118
|
+
effect.state = .active
|
|
119
|
+
effect.isEmphasized = true
|
|
120
|
+
effect.autoresizingMask = [.width, .height]
|
|
121
|
+
effect.wantsLayer = true
|
|
122
|
+
effect.layer?.masksToBounds = true
|
|
123
|
+
effect.layer?.cornerRadius = 16
|
|
124
|
+
effect.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
125
|
+
effect.appearance = NSAppearance(named: .darkAqua)
|
|
126
|
+
contentView = effect
|
|
127
|
+
hosting.autoresizingMask = [.width, .height]
|
|
128
|
+
hosting.frame = effect.bounds
|
|
129
|
+
hosting.wantsLayer = true
|
|
130
|
+
hosting.layer?.isOpaque = false
|
|
131
|
+
hosting.layer?.backgroundColor = NSColor.clear.cgColor
|
|
132
|
+
effect.addSubview(hosting)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
final class NotchHostingView<Content: View>: NSHostingView<Content> {
|
|
137
|
+
override var isOpaque: Bool { false }
|
|
138
|
+
|
|
139
|
+
// The panel is intentionally nonactivating. A click must reach SwiftUI
|
|
140
|
+
// controls immediately, even while another app is the active application.
|
|
141
|
+
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct NotchScreenLayout {
|
|
145
|
+
let edgeInset: CGFloat
|
|
146
|
+
let maximumHeight: CGFloat
|
|
147
|
+
|
|
148
|
+
init(availableHeight: CGFloat, preferredInset: CGFloat = 100) {
|
|
149
|
+
let height = max(1, availableHeight)
|
|
150
|
+
edgeInset = min(preferredInset, max(0, (height - 1) / 2))
|
|
151
|
+
maximumHeight = height - 2 * edgeInset
|
|
152
|
+
}
|
|
153
|
+
}
|